Довольно необычный способ потратить два дня в попытках зашифровать аудиофайл в обычное изображение. О методах, возникших проблемах и результатах читайте ниже.
Предыстория
Под покровом вечера пятницы, поглощая хмельные запасы нашей необъятной и листая любимый Хабр, я наткнулся на плеяды статей о шифровании. Что только куда не зашифровывали, от совсем уж банального шифра Цезаря до менее банального шифрования изображений в аудиофайл. Наслаждаясь достойным вечера чтивом, в голову зашел не разувшись интересный вопрос: "-А кто-нибудь звук в картинку прятал?". Зудящая жажда знаний заставила меня смахнуть с живота остатки кальмаровых колец и сесть за свою рабочую лошадку.
К моему великому удивлению не нашел ничего (Либо этим никто на занимался, либо занимался, но с миром делиться не стал, ну либо я плохо искал. Пятница, вечер, сами понимаете). В любом случае маховик моей решительности уже начал раскручиваться. Итак давайте по порядку. ( полный код проекта здесь ). Примеры для затравочки)):
Метод шифрования
Первым делом я решил продумать саму схему шифрования. Как удобно, экономично и безболезненно можно засунуть звук в изображение? Не буду говорить о всех идеях которые посещали чертоги моего разума, расскажу о методе который я впоследствии и выбрал. Давайте представлю схему для вашего удобства, а потом будут поблочные разъяснения.
Первым делом получаем 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)
sergiks
04.10.2021 20:56Было б здорово перенести снижение качества картинки-копии на качество дешифрованного звука. Скажем, потеря мелких деталей вела бы к снижению верхов, но сохранила бы низы и середину. Отрезать от картинки часть справа или слева — такая же часть записи по длительности пропала бы, но остальное можно было считать.
В общем, переизобрести магнитную ленту )
infund
04.10.2021 21:39Когда-то давно сохранял в jpeg произвольный звук. Использовал, помнится, какие-то незанятые тэги в EXIF. Соответственно, картинка не изменялась, но проиграть этот звук можно было только в моем приложении и ещё было ограничение на объем сохраняемых данных в тэг.
aamonster
04.10.2021 22:26+3Полез в профиль посмотреть возраст автора. Думал – вдруг хвалить надо.
В общем, всё плохо:
Запихать звук в такую картинку – дело нехитрое. Просто взять файл и пришить заголовок (можно использовать ImageMagick). Если хочется шифровать – опять же берутся готовые решения и пишется скрипт строк на 5-10 (ну или однострочник для маньяков)
15 минут на трёхминутный файл – запредельно много.
Про стеганографию читали? Без этого какой смысл ваш звук в картинку пихать? С тем же успехом можно оставить wav/mp3, ну или шифрованный архив с ними.
ZodDZverev
05.10.2021 00:57+8Эммм… напоминает троллейбус из булки хлеба.
Зачем вообще пихать звук в RGB, если на выходе получается какая-то разноцветная дрянь? Можно тогда просто звук зашифровать в бинарник, и черт с ним. В чем ценность вывода результата именно как картинки?
Другое дело, если взять полученную разноцветную дребедень и подмешать её к обычной фотографии, да ещё и так, чтоб это не было заметно. Но при этом, чтоб звук можно было вытащить обратно.
Тогда в этом экзерцисе появляется какой-то минимальный смысл уже.
AgentFire
05.10.2021 01:13+3Тоже ожидал, что звук будет именно СПРЯТАН (как обещает заголовок), в картинку, а не вот этот вот ужас.
DIMooo
05.10.2021 08:01+1Я тоже по заголовку ждал именно этого, а ещё того, что фотку кошечки со звуком можно не сильно отфотошопить, распечатать, помять и поцарапать, перефотать и из полученного результата получить звук, пусть даже с щелчками и искажениями, но понятный.
sergej_pipets
05.10.2021 15:17Ближе аналогия UUE - наличие дополнительного кода видно, но без спецобработки не прочитать.
berez
05.10.2021 02:21+2Автору на заметку: все можно было сделать гораздо проще. Берем любой звуковой файл, приписываем перед ним заголовок Microsoft Bitmap — и получаем прекрасную пестрявую BMP-шку. Единственная задача — выставить в БМП-шке такую ширину и высоту, чтобы файл влез наиболее плотно и без лишних хвостов.
«Извлечение» просто до безобразия: откусываем заголовок BMP и получаем нашу WAV-ку, МР3-шку или даже какой-нибудь FLAC.alexzeed
05.10.2021 09:31+1Более того, мп3 и так будет играть, там синхронизация пофреймовая, заголовок не нужен. Разве что если в заголовке bmp встретится пачка единичек.(старт фрейма), то будет ложный фрейм и в начале немного "хрюкнет".
IronHead
05.10.2021 11:44А можно было пихать звук только в младшие битовые разряды (1,2 и может быть 3 бить в каждом байте) картинки и тогда получилось бы пере изобрести стеганографию.
balamutang
07.10.2021 10:55Можно, но какой смысл?
Звук по толщине потока почти как видео, а информации в нем не очень много.
balamutang
05.10.2021 13:19+2технологии рарджпег сто лет в обед, а в этот рар можно что угодно положить, хоть звук
dcoder_mm
Вот тут еще был красивый способ запихивания звука в изображения, через спектрограмму.
GospodinKolhoznik
aphex twin наоборот засовывал картинки своей нарочито уродливой улыбки в звуковые композиции через спектрограмму.