Аудио-графическое шифрование или как звук в картинку спрятать
Довольно необычный способ потратить два дня в попытках зашифровать аудиофайл в обычное изображение. О методах, возникших проблемах и результатах читайте ниже.
Предыстория
Под покровом вечера пятницы, поглощая хмельные запасы нашей необъятной и листая любимый Хабр, я наткнулся на плеяды статей о шифровании. Что только куда не зашифровывали, от совсем уж банального шифра Цезаря до менее банального шифрования изображений в аудиофайл. Наслаждаясь достойным вечера чтивом, в голову зашел не разувшись интересный вопрос: »-А кто-нибудь звук в картинку прятал?». Зудящая жажда знаний заставила меня смахнуть с живота остатки кальмаровых колец и сесть за свою рабочую лошадку.
К моему великому удивлению не нашел ничего (Либо этим никто на занимался, либо занимался, но с миром делиться не стал, ну либо я плохо искал. Пятница, вечер, сами понимаете). В любом случае маховик моей решительности уже начал раскручиваться. Итак давайте по порядку. (полный код проекта здесь). Примеры для затравочки)):
Секундная запись гугл-марусиА вот десятисекундная запись моего простуженного голоса
Метод шифрования
Первым делом я решил продумать саму схему шифрования. Как удобно, экономично и безболезненно можно засунуть звук в изображение? Не буду говорить о всех идеях которые посещали чертоги моего разума, расскажу о методе который я впоследствии и выбрал. Давайте представлю схему для вашего удобства, а потом будут поблочные разъяснения.
Первым делом получаем 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%! Было бы сто, но мадам частота дискретизации не хочет дешифроваться без потерь.
На этом собственно всё. Мне осталось только пожелать вам удачи и свершений и оставить ссылочки))