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

b0755300187484719767a6b8916b7218.jpg

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

Предыстория

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

a6532e502d7faf882f17fec37e5fe3cd.jpg

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

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

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

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

image-loader.svg

Первым делом получаем 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')

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

image-loader.svg

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

image-loader.svg

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

© Habrahabr.ru