
TL;DR
Я впервые купил на Amazon электронную книгу.
Android-приложение Kindle самой компании Amazon было очень забагованным и часто вылетало.
Попробовал скачать мою книгу, чтобы читать её в реально работающем приложении для чтения.
Осознал, что Amazon больше не позволяет этого делать.
Решил назло выполнить реверс-инжиниринг её системы обфускации.
Обнаружил множество слоёв защиты, в том числе рандомизированные алфавиты.
Победил их все при помощи колдунства с сопоставлением шрифтов.
Часть 1: Amazon вынудила меня объявить вендетту
Тот раз, когда я решил сделать всё по правилам
Годами я «находил» электронные книги. Но в ЭТОТ раз я решил: «Попробую поддержать автора».
Скачал приложение Kindle для Android. Открыл книгу.
Вылет.
Я просто хотел прочитать книгу
Приложение вылетает. Ну ладно, просто воспользуюсь веб-приложением.
Ой, а я не могу скачать его для офлайн-чтения. Что, если я нахожусь в самолёте?
Постойте-ка, книгу даже нельзя экспортировать в Calibre? Туда, где я храню ВСЕ свои остальные книги?
Подведём итог:
Я заплатил деньги за эту книгу.
Её можно читать только в поломанном приложении Amazon.
Я не могу её скачать.
Не могу создать её резервную копию.
На самом деле, она мне не принадлежит.
Amazon может удалить её, когда пожелает.
Это прокат, а не покупка.

Это уже личное
Я мог бы вернуть за книгу деньги и «найти» её кое-где ещё за полминуты. Так было бы проще.
Но смысл не в этом.
Смысл в том, что Я ЗАПЛАТИЛ ЗА ЭТУ КНИГУ. Она моя. И я собираюсь читать её в Calibre с остальной частью моей библиотеки, даже если для этого мне понадобится выполнить реверс-инжиниринг веб-клиента Amazon.
Время для реверсинга
Kindle Cloud Reader (веб-версия) работает. Изучая сетевые запросы, я наткнулся на следующее:
https://read.amazon.com/renderer/renderЧтобы что-то скачать, нужны:
1. Куки сессии — стандартный логин Amazon.
2. Токен рендеринга — из вызова API startReading.
3. Токен сессии ADP — дополнительный слой аутентификации.
При отправке тех же заголовков и куки браузер возвращает файл TAR.
Что внутри TAR?
page_data_0_4.json # «Текст» (спойлер: на самом деле, это не текст)
glyphs.json # SVG-определения для каждого символа
toc.json # Содержание
metadata.json # Информация о книге
location_map.json # Привязки позицийЧасть 3: слои обфускации Amazon из ада электронных книг
Я скачал первые несколько страниц, ожидая увидеть текст, но вместо них получил это:
{
"type": "TextRun",
"glyphs": [24, 25, 74, 123, 91, 18, 19, 30, 4, ...],
"style": "paragraph"
}Это не буквы, а ID глифов. Символ «T» — это не 84 в Unicode, а его глиф 24.
А глиф 24 — это просто последовательность чисел, определяющая контуры, то есть изображение буквы.
Это подстановочный шифр! Каждый символ сопоставляется с идущими не по порядку ID глифа.
Алфавит меняется. Каждые пять страниц.
Я скачал следующий набор страниц. Та же самая буква «T» теперь имеет глиф 87.
В следующем наборе это уже глиф 142.
Они рандомизируют весь алфавит при КАЖДОМ запросе.
Это означает, что:
Можно получать только по пять страниц за раз (жёсткое ограничение API).
Каждый запрос получает совершенно новые привязки глифов.
ID глифов одного запроса не имеют никакого смысла в других запросах.
Невозможно создать единую таблицу сопоставления для всей книги.
Покажу, насколько всё плохо
Для моей 920-страничной книги:
Требуется 184 отдельных запроса к API.
Нужно взломать 184 разных случайных алфавита.
Обнаружен 361 уникальный глиф (a-z, A-Z, знаки пунктуации, лигатуры).
Суммарно нужно декодировать 1051745 глифов.
Поддельный хинтинг шрифтов (они заметают следы)
Некоторые контуры SVG содержали вот такой мусор:
M695.068,0 L697.51,-27.954 m3,1 m1,6 m-4,-7 L699.951,-55.908 ...Мы видим маленькие команды m3,1 m1,6 m-4,-7, это микрооперации MoveTo.
Что в этом коварного:
Браузеры обрабатывают их без проблем (нативный Path2D).
SVG-библиотеки Python создают ложные соединительные линии.
При наивном рендеринге глифы выглядят повреждёнными.
Это препятствует методикам сэмплирования контуров.
Это намеренная защита от скрейпинга. В браузере рендерятся идеально, но таким образом, что мы не можем сравнивать контуры в парсере.
Взгляните

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

Множественные варианты шрифтов
Он не просто один, их ЧЕТЫРЕ варианта:
bookerly_normal (99% глифов);
bookerly_italic (выделение);
bookerly_bold (заголовки);
bookerly_bolditalic (выделенные заголовки).
Плюс специальные лигатуры: ff, fi, fl, ffi, ffl
Больше вариаций = больше уникальных глифов, которые нужно взломать = больше мучений.
Оптическое распознавание справляется так себе (моя неудавшаяся попытка)
Я попробовал выполнить оптическое распознавание символов отрендеренных глифов. Результаты:
Распознано 178/348 глифов (51%).
С 170 глифами полный провал.
Программы OCR ужасно справляются с отдельными символами без контекста. Они путают «l» с «I» и «1». Не могут обрабатывать пунктуацию. Совершенно отказываются работать с лигатурами.
Вероятно, для хорошей работы OCR требуются слова и предложения.
Часть 4: решение, которое действительно работает
В каждом запросе содержится glyphs.json с определениями SVG-контуров:
{
"24": {
"path": "M 450 1480 L 820 1480 L 820 0 L 1050 0 L 1050 1480 ...",
"fontFamily": "bookerly_normal"
},
"87": {
"path": "M 450 1480 L 820 1480 L 820 0 L 1050 0 L 1050 1480 ...",
"fontFamily": "bookerly_normal"
}
}ID глифов меняются, но SVG-фигуры остаются прежними.
Почему не удалось сравнивать SVG напрямую
Первая попытка: нормализация и сравнение координат SVG-контуров.
Закончилась неудачей, потому что:
Координаты немного варьируются
Команды контуров задаются по-разному
Попиксельно-точное сопоставление
К чёрту сравнение координат. Давайте просто отрендерим всё и будем сравнивать пиксели.

1. Рендерим каждый SVG в виде изображения
Используем библиотеку cairosvg (она позволяет корректно обрабатывать этот поддельный хинтинг шрифтов).
Для обеспечения точности выполняем рендеринг с разрешением 512 x 512px.
2. Генерируем перцептивные хэши
Хэшируем каждое отрендеренное изображение.
Хэш превращается в уникальный идентификатор.
Одинаковая форма = одинаковый хэш, вне зависимости от ID глифа.
3. Создаём пространство нормализованных глифов
Сопоставляем все 184 случайных алфавита с ID хэшей.
Теперь глиф «a1b2c3d4...» всегда означает букву «T».
4. Выполняем сопоставление с символами
Скачиваем TTF-шрифты Bookerly.
Рендерим каждый символ (A-Z, a-z, 0-9, пунктуация).
Используем SSIM (Structural Similarity Index) для сопоставления.
Почему SSIM идеально подходит для этой задачи
SSIM сравнивает не пиксели напрямую, а структуру изображения. Он справляется с:
Незначительными различиями в рендеринге.
Вариациями антиалиасинга.
Незначительными проблемами масштаба.
Для каждого неизвестного глифа мы находим в TTF символ с наибольшим значением SSIM. Это и будет наша буква.
Обработка пограничных случаев
Лигатуры: ff, fi, fl, ffi, ffl
Это одинарные глифы для н��скольких символов
Пришлось добавлять их в библиотеку TTF вручную
Особые символы: длинное тире, кавычки, маркеры списков
Расширенный набор символов, выходящий за рамки основного ASCII
Сопоставление с полным диапазоном Unicode в Bookerly
Варианты шрифтов: полужирный, курсив, полужирный курсив
Создание отдельных библиотек для каждого варианта
Сопоставление со всеми библиотеками, выбор наилучшей оценки
Часть 5: момент, когда всё заработало
Финальная статистика
=== ЭТАП НОРМАЛИЗАЦИИ ===
Общее количество обработанных наборов: 184
Количество найденных уникальных глифов: 361
Общее количество глифов в книге: 1051745
=== ЭТАП СОПОСТАВЛЕНИЯ ===
Выполнено успешное сопоставление 361/361 уникальных глифов (100,00%)
Неудачное сопоставление: 0 глифов
Средняя оценка SSIM: 0,9527
=== ДЕКОДИРОВАННЫЙ ВЫВОД ===
Общее количество символов: 5623847
Страниц: 920Идеально. Мы корректно декодировали все символы.
Воссоздание EPUB с идеальным форматированием
В JSON содержится позиционирование для каждого прогона текста:
{
"glyphs": [24, 25, 74],
"rect": {"left": 100, "top": 200, "right": 850, "bottom": 220},
"fontStyle": "italic",
"fontWeight": 700,
"fontSize": 12.5,
"link": {"positionId": 7539}
}Я воспользовался им, чтобы сохранить:
Разрывы параграфов (изменения координат Y)
Выравнивание текста (паттерны по координатам X)
Стилизацию полужирным/курсивом
Размеры шрифтов
Внутренние ссылки
Получившийся EPUB почти неотличим от оригинала!
Заключение
Amazon приложила серьёз��ые усилия к своей веб-обфускации.
Оправданы ли были мои мучения?
Для того, чтобы прочитать одну книгу? Нет.
Для того, чтобы отстоять свою точку зрения? Разумеется.
Чтобы узнать о рендеринге SVG, перцептивном хэшировании и метриках шрифтов? Вероятно, да.
Пользуйтесь этим знанием ответственно
Применять его можно только для резервного копирования КУПЛЕННЫХ книг.
Пожалуйста, не подавайте на меня в суд и не оставляйте без штанов.
Если вы каким-то образом связаны с Amazon, то свяжитесь со мной по адресу pixelmelt + at + protonmail.com.
