Довольно необычный способ потратить два дня в попытках зашифровать аудиофайл в обычное изображение. О методах, возникших проблемах и результатах читайте ниже.

Предыстория

Под покровом вечера пятницы, поглощая хмельные запасы нашей необъятной и листая любимый Хабр, я наткнулся на плеяды статей о шифровании. Что только куда не зашифровывали, от совсем уж банального шифра Цезаря до менее банального шифрования изображений в аудиофайл. Наслаждаясь достойным вечера чтивом, в голову зашел не разувшись интересный вопрос: "-А кто-нибудь звук в картинку прятал?". Зудящая жажда знаний заставила меня смахнуть с живота остатки кальмаровых колец и сесть за свою рабочую лошадку.

К моему великому удивлению не нашел ничего (Либо этим никто на занимался, либо занимался, но с миром делиться не стал, ну либо я плохо искал. Пятница, вечер, сами понимаете). В любом случае маховик моей решительности уже начал раскручиваться. Итак давайте по порядку. ( полный код проекта здесь ). Примеры для затравочки)):

Секундная запись гугл-маруси
Секундная запись гугл-маруси
А вот десятисекундная запись моего простуженного голоса
А вот десятисекундная запись моего простуженного голоса

Метод шифрования

Первым делом я решил продумать саму схему шифрования. Как удобно, экономично и безболезненно можно засунуть звук в изображение? Не буду говорить о всех идеях которые посещали чертоги моего разума, расскажу о методе который я впоследствии и выбрал. Давайте представлю схему для вашего удобства, а потом будут поблочные разъяснения.

Первым делом получаем samplerate(частота дискретизации) и data(значения пиков амплитуды аудиодорожки) с помощью scipy.io.wavfile. В коде на Python это выглядит так:

srate , data = wavfile.read(file)

Теперь второй шаг. Проходимся циклом по списку data и из каждого значения создаём hex-code.Например, из значения 544 получаем #544aaf и так далее. Буквенные значения являются солью(salt) и создаются рандомно. Здесь кстати обнаруживается первая проблема. Значения амплитуды могут принимать отрицательные значения, hex-code же минусы не жалует. Решается просто, заменяем '-' на 'f' и будем обращать внимание на этот флаг при дешифровке. Конечная реализация выглядит так:

for elem in s_arr:   
        gate = np.random.choice([False, True])
        app = None
        salt_pos = ''.join([np.random.choice(buffer_symbols) for _ in range(6-len(str(elem)))])
        salt_neg = ''.join([np.random.choice(buffer_symbols) for _ in range(6-len(str(elem)))])
        
        if elem >= 0:
            
            if gate:
                app = f'#{elem}' + salt_pos
                new_arr.append(app)
            else:
                app = f'#{salt_pos}{elem}'
                new_arr.append(app)  
        else:
            
            if gate:
            
                app = f'#f{elem*-1}' + salt_neg
                new_arr.append(app)
                
            else:
                app = f'#f{salt_neg}{elem*-1}'
                new_arr.append(app)

В этом же шаге переводим значения hex-code из new_arr в RGB формат. Это и есть цвета пикселей будущего изображения. Не забываем зашифровать значение samplerate в последний пиксель. Далее переводим наш np.array массив в квадратную размерность и создаем из него изображения с помощью Image.fromarray библиотеки PIL(Pillow):

p_arr = np.array([list(hex2rgb(x)) for x in new_arr] + [[0,0,0] for x in range(delta_res - 1)] +[[srate_rgb, srate_rgb, srate_rgb]])
p_arr = p_arr.reshape(resolution, resolution, 3)
p_arr = p_arr.astype(np.uint8)
    
img = Image.fromarray(p_arr)
img.save(f'{file[:-4]}_encoded.png')

Обернув всё вышеописанное в функцию получаем:

def encode(file: str) -> PIL.PngImagePlugin.PngImageFile:
    
    """ Audio track encoding function.
        It takes a .wav file as input and encodes first in HEX and then in RGB.
        The output is an image with audio encoded in it
    """
    srate , s_arr = wavfile.read(file)
    # Разрешение это квадратный корень из длины списка значений амплитуд(s_arr, data)
    # Не спрашивайте почему, мне это показалось очень удобным
    resolution = math.ceil(np.sqrt(len(s_arr)))
    delta_res = resolution**2 - len(s_arr)
    
    new_arr = []

    buffer_symbols = ['a', 'b', 'c', 'd', 'e']
    
    # Так как частота дискретизации число большое, берем из неё кубический корень
    srate_rgb = int(srate ** (1/3))
    
    # А вот и цикл преобразования значений амплитуд в hex-code
    for elem in s_arr:   
        gate = np.random.choice([False, True])
        app = None
        
        # Значения солей случайны и разные для отрицательных и положительных значений
        salt_pos = ''.join([np.random.choice(buffer_symbols) for _ in range(6-len(str(elem)))])
        salt_neg = ''.join([np.random.choice(buffer_symbols) for _ in range(6-len(str(elem)))])
        
        if elem >= 0:
            
            if gate:
                app = f'#{elem}' + salt_pos
                new_arr.append(app)
            else:
                app = f'#{salt_pos}{elem}'
                new_arr.append(app)  
        else:
            
            if gate:
            
                app = f'#f{elem*-1}' + salt_neg
                new_arr.append(app)
                
            else:
                app = f'#f{salt_neg}{elem*-1}'
                new_arr.append(app)
            																																													# Зашиваем частоту дискретизации																																								# 
    p_arr = np.array([list(hex2rgb(x)) for x in new_arr] + [[0,0,0] for x in range(delta_res - 1)] + [[srate_rgb, srate_rgb, srate_rgb]])
    # Меняем размерность
    p_arr = p_arr.reshape(resolution, resolution, 3)
    p_arr = p_arr.astype(np.uint8)
    
    # Создаём изображение
    img = Image.fromarray(p_arr)
    img.save(f'{file[:-4]}_encoded.png')

Вот как то так. На выходе получаем изображение, с одной стороны мешанина из пикселей, с другой, что то она мне напоминает(Спектрограмма? Возможно).В любом случае, без функции дешифровки это всего лишь хоть и красивая, но бесполезная пнгэшка. Кстати о дешифровке. План тот же, схема, а потом пояснения с кодом.

Получая на вход изображение с зашифрованным аудио внутри, функция decode() первым делом получает np.array значений RGB этой картинки и вытягивает его в одномерный массив. Далее она проворачивает прямо противоположное функции encode():

def decode(path: str):
    
    """Audio decoding function from image. Uses the inverse algorithm of the encode () function
    """
    
    # Получаем и вытягиваем в одномерку наше изображение
    img = np.array(Image.open(path))
    img = img.reshape(img.shape[0]**2, 3)
    
    f_arr = []
    end_arr = []
    
    # Обратный цикл( В народе дешифровка )
    for elem in img:
        f_arr.append(rgb2hex(*elem))
        
    for h in f_arr:
        
        res = None
        
        # А вот и наш флаг f , если он есть подставляем к конечному значению минус
        if h[1].lower() == 'f':
          
          	# Единтсвенными числами в получившемся hex-code будут нужные нам значения амплитуд
            # Используем модель re для вытягивания циферок
            res = re.findall('\d+', h)[0]
            res = -int(res)
            end_arr.append(res)
            
        else:
            
            res = re.findall('\d+', h)[0]
            end_arr.append(int(res))
            
    
    
    end_arr = np.array(end_arr).astype(np.int16)
    
    # Помните последний пиксель с зашитой в него частотой дискретизации? 
    # Восстанавливаем эту мадам в звании
    samplerate = img[-1][-1] ** 3
    samplerate -= samplerate % -100
    
    # Создаём .wav файл из полученных np.array со значениями амлитуд и частоты диск-ии
    wavfile.write(f'{path[:-4]}_decoded.wav', samplerate, end_arr)

Послесловие, итоги, оговорки и ограничения

Мда, получилось конечно здорово. Время потрачено не зря. Но. Давайте о минусах:

Первое. Я пока пока не решил проблему формата и поэтому код работает только с .wav файлами.

Второе. Алгоритм шифровки/дешифровки не самый быстрый, аудио длиной три минуты кодируется минут 15, более того, разрешение выходного изображения также напрямую зависит от длительности аудиофайла. Тот же трёхминутный фрагмент в зашифрованном виде имеет разрешение 3058х3058 пикселей. Не слабо. Но зато потери минимальные! при сравнении оказалось что исходный и дешифрованный файлы идентичны на 99.3%!! Было бы сто, но мадам частота дискретизации не хочет дешифроваться без потерь.

На этом собственно всё. Мне осталось только пожелать вам удачи и свершений и оставить ссылочки))

Комментарии (18)


  1. dcoder_mm
    04.10.2021 19:04
    +1

    Вот тут еще был красивый способ запихивания звука в изображения, через спектрограмму.


    1. GospodinKolhoznik
      04.10.2021 19:25
      +2

      aphex twin наоборот засовывал картинки своей нарочито уродливой улыбки в звуковые композиции через спектрограмму.


  1. sergiks
    04.10.2021 20:56

    Было б здорово перенести снижение качества картинки-копии на качество дешифрованного звука. Скажем, потеря мелких деталей вела бы к снижению верхов, но сохранила бы низы и середину. Отрезать от картинки часть справа или слева — такая же часть записи по длительности пропала бы, но остальное можно было считать.
    В общем, переизобрести магнитную ленту )


  1. Scratch
    04.10.2021 21:18
    +8

    Это не шифрование, это кодирование


  1. infund
    04.10.2021 21:39

    Когда-то давно сохранял в jpeg произвольный звук. Использовал, помнится, какие-то незанятые тэги в EXIF. Соответственно, картинка не изменялась, но проиграть этот звук можно было только в моем приложении и ещё было ограничение на объем сохраняемых данных в тэг.


  1. aamonster
    04.10.2021 22:26
    +3

    Полез в профиль посмотреть возраст автора. Думал – вдруг хвалить надо.

    В общем, всё плохо:

    1. Запихать звук в такую картинку – дело нехитрое. Просто взять файл и пришить заголовок (можно использовать ImageMagick). Если хочется шифровать – опять же берутся готовые решения и пишется скрипт строк на 5-10 (ну или однострочник для маньяков)

    2. 15 минут на трёхминутный файл – запредельно много.

    3. Про стеганографию читали? Без этого какой смысл ваш звук в картинку пихать? С тем же успехом можно оставить wav/mp3, ну или шифрованный архив с ними.


  1. ZodDZverev
    05.10.2021 00:57
    +8

    Эммм… напоминает троллейбус из булки хлеба.

    Зачем вообще пихать звук в RGB, если на выходе получается какая-то разноцветная дрянь? Можно тогда просто звук зашифровать в бинарник, и черт с ним. В чем ценность вывода результата именно как картинки?

    Другое дело, если взять полученную разноцветную дребедень и подмешать её к обычной фотографии, да ещё и так, чтоб это не было заметно. Но при этом, чтоб звук можно было вытащить обратно.

    Тогда в этом экзерцисе появляется какой-то минимальный смысл уже.


    1. AgentFire
      05.10.2021 01:13
      +3

      Тоже ожидал, что звук будет именно СПРЯТАН (как обещает заголовок), в картинку, а не вот этот вот ужас.


    1. DIMooo
      05.10.2021 08:01
      +1

      Я тоже по заголовку ждал именно этого, а ещё того, что фотку кошечки со звуком можно не сильно отфотошопить, распечатать, помять и поцарапать, перефотать и из полученного результата получить звук, пусть даже с щелчками и искажениями, но понятный.


    1. sergej_pipets
      05.10.2021 15:17

      Ближе аналогия UUE - наличие дополнительного кода видно, но без спецобработки не прочитать.


  1. berez
    05.10.2021 02:21
    +2

    Автору на заметку: все можно было сделать гораздо проще. Берем любой звуковой файл, приписываем перед ним заголовок Microsoft Bitmap — и получаем прекрасную пестрявую BMP-шку. Единственная задача — выставить в БМП-шке такую ширину и высоту, чтобы файл влез наиболее плотно и без лишних хвостов.
    «Извлечение» просто до безобразия: откусываем заголовок BMP и получаем нашу WAV-ку, МР3-шку или даже какой-нибудь FLAC.


    1. ZodDZverev
      05.10.2021 05:22
      +1

      Ты не понимаешь!!!


    1. alexzeed
      05.10.2021 09:31
      +1

      Более того, мп3 и так будет играть, там синхронизация пофреймовая, заголовок не нужен. Разве что если в заголовке bmp встретится пачка единичек.(старт фрейма), то будет ложный фрейм и в начале немного "хрюкнет".


  1. Nehc
    05.10.2021 09:43

    >>>-А кто-нибудь звук в картинку прятал?

    Чуть более чем все.


  1. IronHead
    05.10.2021 11:44

    А можно было пихать звук только в младшие битовые разряды (1,2 и может быть 3 бить в каждом байте) картинки и тогда получилось бы пере изобрести стеганографию.


    1. balamutang
      07.10.2021 10:55

      Можно, но какой смысл?

      Звук по толщине потока почти как видео, а информации в нем не очень много.


  1. balamutang
    05.10.2021 13:19
    +2

    технологии рарджпег сто лет в обед, а в этот рар можно что угодно положить, хоть звук


  1. singeorange
    16.10.2021 17:21
    +1

    Мой старый фотик умел записывать в картинку звуковой комментарий.