Аудио-графическое шифрование или как звук в картинку спрятать

от автора

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

Предыстория

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

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

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

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

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

Первым делом получаем 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%!! Было бы сто, но мадам частота дискретизации не хочет дешифроваться без потерь.

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


ссылка на оригинал статьи https://habr.com/ru/articles/581496/