Недавно нас окатило волной громких скандалов, разразившихся словно гром среди ясного неба, одним из самых громких среди них было раскрытие программы PRISM.
Итак, давайте представим такую ситуацию: есть два человека, которые хотят обмениваться сообщениями с секретной информацией, есть люди, желающие заполучить эту информацию. Одним из самых разумных решений данной проблемы может стать обмен сообщениями через открытые источники с использованием стеганографии.
Например, пользователь1 оставляет на форуме смищную гифку с милым котэ, в недрах которой скрывается какое-то тайное послание. Для человеческого глаза, скорей всего, будет незаметно легкое искажение цветов, которое вполне можно списать на низкое качество картинки, а вот пользователь2, обладающий специальной программой и ключем без проблем получит из картинки информацию.
[Intro]​
Для начала давайте поговорим о теории.
Стеганография, которой посвящена данная статья, это, по сути, искуство сокрытия информации от посторонних глаз.
Существует множество разных способов спрятать данные на виду, мы же прибегнем к модифицированному LSB(Least Significant Bit, наименьший значащий бит, подробнее о нем можно почитать в Википедии, скажу лишь, что данные при таком подходе прячутся в последние значащие биты, таким образом очень слабо влияя на значения байтов): будем менять 4 наименее значащих бита. Да, это намного более значительные изменения, но как показывает практика, изменение в значении цвета на 15(из 255 возможных) пунктов легко пропустить, не имея оригинала, что бы сравнить.
Вот кстати пример:
--
одно из этих изображений оригинал, другое копия с небольшим посланием.
[Кодим]​
Я буду писать на питоне, этот язык понятен почти всем, даже тем, кто его не знает. Код писался и тестировался под python3, как будет работать под второй веткой неизвестно.
Для начала можно ознакомиться с описанием формата вот тут Там все очень кратко, но достаточно полезно для понимания кода. Суть нашего кода будет предельно простой: зашифровать сообщение(максимальная длинна 384 байта) и положить его в главную палитру файла.
В формате используются битовые поля, но в питоне, как оказалось, нет стандартной из реализации, поэтому пришлось городить свой костыль:

Текст

class BitArray:
    def __init__(self, val=None, length=0):
        bits=[]
        if type(val) is int:
           
            while(val!=0):
                if val&1:
                    bits+=[1]
                else:
                    bits+=[0]
                val>>=1
               
        elif type(val) is list:
            bits=list(val)
        if bits==[]:
            bits=[0]
           
        if length!=0:
            if length>=len(bits):
                bits=bits+[0]*(length-len(bits))
            else:
                raise Exception("val too long")
       
        self._val=bits
    #индексатор, возвращающий объект BitArray
    def __getitem__(self, item):
        return self.__class__(self._val[item])
    #длинна в битах
    def __len__(self):
        return len(self._val)
    #исключительно для отладки, наглядное представление объекта для интерактивного режима
    def __repr__(self):
        bits=self._val
        bits.reverse()
        return str(self.__class__)+" Bits: "+"".join(str(x) for x in bits)+"; val: "+str(int(self))
    #приведение к int
    def __int__(self):
        res=0
        for i in range(len(self._val)):
            res|=self._val[i]<<i
        return res
   
    def __add__(self, ba):
        self._val+=ba._val
        return self

Поскольку писать универсальный массив не хотелось, в классе только необходимый для конкретно этого проекта функционал, ничего лишнего.
Теперь давайте опишем такой же минималистичный класс для работы с gif-файлом:

текст

class Gif:
    #http://www.onicos.com/staff/iz/formats/gif.html
    def __init__(self, data):
        self._header=data[:6]
        self._info=data[6:12]
       
        info=BitArray(self._info[4])
       
        if int(info[0])!=1:
            raise Exception("This file haven't global pallet")
        tblsize=2**(1+int(info[5:]))
       
        pallet=data[12:12+tblsize*3]
        self._data=data[12+tblsize*3:]#все, с чем не умеем работать храним в неизменном виде
        self._pallet=pallet
       
    def putData(self, data):
        data=list(data)
        maxlen=len(self._pallet)*.5 #максимальная длинна данных, которые вместит файл
        data=list(len(data).to_bytes(2,"little"))+data #добвляем в начало длинну данных
       
        if maxlen>=len(data):
            data+=[0]*int(maxlen-len(data)) #при необходимости дополняем нулями наше смообщение
        else:
            raise Exception("Can't insert {0} bytes of data in {1}".format(len(data),maxlen))
       
        rdata=[]
        for d in data: #разбиваем каждый байт на две части
            ba=BitArray(d)
            rdata+=[int(ba[:4]),int(ba[4:])]
        data=rdata
        self._pallet=[(int(x) & 0b11110000) for x in list(self._pallet)] #очищаем младшие биты каждого байта в палитре
        self._pallet=[(self._pallet[i]|data[i]) for i in range(len(data))] #ложим данные в палитру
    def getData(self):
        data=[(self._pallet[i]&0xf) for i in range(len(self._pallet))] #удаляем ненужные нам биты
        data=[int(BitArray(data[i],4)+BitArray(data[i+1],4)) for i in range(0,len(data),2)] #складываем байты из двух частей
        l=int.from_bytes(data[:2],"little")
       
        return data[2:2+l]
    def construct(self):
        #собираем файл из частей
        return self._header+self._info+bytes(self._pallet)+self._data

Надеюсь, коментов в коде достаточно для его понимания, но если нет -- задавайте вопросы, на все отвечу.
[Шифруемся]​
Поскольку одной из целей было создать код, не зависящий от сторонних библиотек, а в питоне нет ничего кроме хешфункций, было принято решение просто гаммировать данные хитрым хешем пароля:

текст

import hashlib
#генератор гаммы для шифрования
#не лучший вариант, но и не худший:)
def GammaString(secret):
    if type(secret) is str:
        secret=secret.encode()
    md5=hashlib.md5()
    sha=hashlib.sha512()
    md5.update(secret)
    sha.update(secret)
   
    while True:
        digest=md5.digest()+sha.digest()
        for d in digest:
            yield d
        md5.update(digest)
        sha.update(digest)
#опять таки не лучший вариант
def xor(data, secret):
    if type(data) is str:
        data=data.encode()
    gamma=GammaString(secret)
    return bytes(d^next(gamma) for d in data)

Коротко о сути работы GammaString: этот генератор сначала берет md5 и sha512 хэши пароля, после чего складывает их в один digest и посимвольно отдает в функцию гаммирования, когда этот digest заканчивается, берется его md5 и sha512 хеши и складываются в новый digest и все начинается с начала, таким образом мы получаем почти бесконечную довольно качественную гамму.
[Интерфейс]​
Поскольку это просто заготовка, интерфейс будет консольным, за него отвечает следующий код:

текст

import chipher
from giffile import Gif
import sys
usage="""
Using {0}:
    {0} <put|get> <-s"password"|-sf"pwdfile"> [<-d"data string"|-df"datafile">] <-g"gif container"> [<-o"out file name">]
Parameters:
    put \t put data to container
    get \t get data from container
    -s"password" \t set chiping password
    -sf"pwdfile" \t using as pwd content of file
    -d"data string" \t set chiping text, must be ignored when get
    -df"datafile" \t using text of file as chiping text, must be ignored when get
    <-g"gif container"> \t set gif container
    <-o"out file name"> \t set output, if ignored put out to console
"""
def main(argv):
    gif=""
    data=""
    secret=""
    out=""
    action="put"
 
    for arg in argv:
        if arg.startswith("-s"):
            secret=arg[3:-1]
        elif arg.startswith("-d"):
            data=arg[3:-1]
        elif arg.startswith("-sf"):
            secret=open(arg[3:-1],"rb").read()
        elif arg.startswith("-df"):
            data=open(arg[3:-1],"rb").read()
        elif arg.startswith("-g"):
            gif=open(arg[3:-1],"rb").read()
        elif arg.startswith("-o"):
            out=arg[3:-1]
        elif arg=="put":
            action="put"
        elif arg=="get":
            action="get"
    if gif=="" or secret=="":
        print(usage)
        return
    gif=Gif(gif)
    res=b""
   
    if action=="put":
        if data=="":
            print(usage)
            return
       
        data=chipher.xor(data,secret)
        gif.putData(data)
        res=gif.construct()
    elif action=="get":
        res=chipher.xor(gif.getData(),secret)
    if out=="":
        try:
            print(res.decode())
        except:
            print("Can't decode content as string, use -o parameter")
            print(res)
            return
    else:
        with open(out,"wb") as f:
            f.write(res)
if __name__=="main":
    main(sys.argv)

Как видите, все весьма просто. Останавливаться на нем, я пожалуй не буду, код кривой, но вроде рабочий.
[В заключение]​
Определить наличие в файле зашифрованных скрытых данных может быть весьма сложно, особенно не зная пароля. Попробуйте угадать в какой из картинок скрыто сообщение(просто) и какое(сложнее). Подсказка: там секретные данные.