После завершения покупки TicketMaster сообщил мне, что я не смогу распечатать билеты на мероприятие. Сайт выпускает билеты при помощи системы Mobile Entry, он же SafeTix. Они имеют вид обновляемого штрих-кода, отображаемого в веб-приложении или приложении для Android/iOS TicketMaster.
«Скриншоты не позволят вам пройти», зато позволят инструменты разработчика Chrome
Возможно, я старею, но мне ещё помнятся времена, когда распечатываемые билеты использовались повсюду. Покупатель мог распечатать билеты после приобретения онлайн или даже в кассе (ничего себе!), и принести эти бумажные билеты ко входу на мероприятие. Их можно было сохранять как PDF и просматривать практически на любом устройстве. PDF-билетами можно пользоваться, даже когда у телефона нет подключения к Интернету. Бумажными билетами можно пользоваться, даже когда у тебя нет телефона. Если я покупал билет в официально продающей их компании (а не у подозрительного посредника), то точно знал, что он настоящий. Не было никакой опасности, что тебя с этим билетом не пустят. Можно было спокойно отправлять их друзьям по WhatsApp, iMessage, Signal, электронной почте или даже передавать распечатанные билеты из рук в руки.
А эти обновляемые штрих-коды далеко неидеальны. Я лично столкнулся с этим в прошлом году, когда пошёл на очень популярный концерт, где использовалась похожая система с билетами на основе обновляемых QR-кодов. Множество людей, включая и меня с моими друзьями, застревали на турникете из-за кучи проблем с поломанными штрих-кодами. Самая главная проблема:
У телефона нет подключения к Интернету, поэтому QR-код не грузится.
На мероприятии была так много народу, что сотовые вышки и WiFi были перегружены. Доступ к Интернету был неуверенным и прерывистым.
Очевидно, что у компании, создавшей этот билетный кошмар (не помню её названия), не было телефонной линии поддержки, а даже если бы и была, то, вероятно, она бы не работала. Сотрудники на мероприятии ничего не могли поделать. Нам оставалось только размахивать телефоном в воздухе и надеяться на то, что билетное приложение рано или поздно обновится.
В конечном итоге, мне, к счастью, удалось «поймать» какого-то сотового оператора и загрузить QR-коды билетов. Мы оставили за турникетами кучу других владельцев билетов, размахивавших телефонами. Понятия не имею, удалось ли им попасть внутрь.
За опыт пользования этой высокотехнологичной системой я выложил триста долларов.
Маркетинг
TicketMaster рекламирует свою технологию SafeTix как панацею от мошенников и спекулянтов.
В SafeTix™ использован новый уникальный штрих-код, автоматически обновляемый каждые несколько секунд, чтобы его нельзя было украсть или копировать; это обеспечивает защиту и надёжность ваших билетов.
В Ticketmaster SafeTix применяется новая уникальная система штрих-кодов, автоматически обновляемых каждые 15 секунд. Это сильно снижает опасность мошенничества с украденными или подделанными билетами.
Наша защищённая технология создания билетов снижает риск мошенничества с билетами, устраняя возможность кражи и подделки. Купив мобильные билеты на Ticketmaster, вы можете быть уверены, что вам достанутся места, за которые вы заплатили.
А ещё там есть вот такой шедевр:
Если присмотреться к билету, можно заметить, что в нём есть движущаяся полоса, делающая его в каком-то смысле живым. Это та самая технология, ежесекундно работающая над вашей защитой.
Ой, не гони, TicketMaster. Это просто CSS-анимация, имей совесть.
Меня насторожило вот это:
Штрих-код на мобильном билете включает в себя технологию защиты, то есть скриншоты и распечатки билета невозможно будет отсканировать.
Это вызвало у меня флэшбеки с прошлогоднего концерта: я представил, что ещё отчаяннее размахиваю телефоном, молясь о подключении к сотовой сети больше, чем Сол Гудман в пустыне.
Но TicketMaster был готов развеять мои тревоги:
Беспокоитесь о работе сотовой сети на мероприятии? Билет решит эту проблему. Если вы просмотрите его в приложении, то билет автоматически сохранится и всегда будет готов.
Отлично, если я доверюсь приложению и оно не поломается в день концерта, то всё будет в порядке. К сожалению, я ему не доверяю, к тому же я не хочу устанавливать это шпионское ПО на свой телефон.
Мотивация
Причины продвижения этой технологии компанией TicketMaster достаточно очевидны:
- SafeTix усложняет процесс перепродажи билетов вне его замкнутого высокодоходного маркетплейса перепродаж, на котором компания может покупать по дешёвке и продавать подороже людям, не имеющим альтернативы.
- Он заставляет пользователей устанавливать проприетарное приложение TicketMaster с закрытыми исходниками, что даёт компании больше информации об устройствах и поведении пользователей.
- Покупатели не могут сохранять и передавать билеты вне Ticketmaster. Это заставляет владельцев билетов передавать TicketMaster контактную информацию друзей, что можно использовать для создания социальных графов и снижения конфиденциальности.
TicketMaster ни за что не признается в такой мотивации, но эти последствия несомненно проявляются, даже если у компании не было таких намерений; и всё это весьма радует совладельцев TicketMaster, но не покупателей.
Противоречие
Если у вас есть опыт работы с компьютерами и ПО, то после прочтения маркетинговых заявлений TicketMaster у вас мог возникнуть тот же вопрос, что и у меня.
Как билеты можно сохранять офлайн, если их нельзя передавать за пределы системы TicketMaster?
Билет цифровой. Сохранение данных офлайн аналогично их копированию на жёсткий диск. Если данные можно скопировать, то их можно и передать. А если можно передать, то можно и продать/
Это противоречит заявлениям TicketMaster. Нельзя создать надёжную DRM для билетов, если эти билеты можно просматривать офлайн.
Так что же на самом деле делает TicketMaster для создания этих обновляемых штрих-кодов?
Реверс-инжиниринг
Первым делом я должен был изучить сами штрих-коды, чтобы понять, что из них можно выудить. Их формат достаточно прост. Это штрих-коды PDF417, кодирующие текст в UTF-8. Как говорилось выше, эта плавающая по штрих-коду синяя полоска — просто бесполезная CSS-анимация: на самом деле она не мешает сканированию скриншотов штрих-кода, потому что PDF417 имеет встроенные свойства коррекции ошибок.
Похоже, некоторые более старые штрих-коды кодировали другие форматы текста, но штрих-коды в моём веб-приложении TicketMaster генерировали примерно такие данные embed
:
B4cq2BdFCpFl90TDuYD3pWfRDSO6eQ3bR0YQqsDnyfciuVFkKp+m0zI+a2lgfonY::140013::481994::1707070843
Примечание: данные взяты не из реального штрих-кода SafeTix. Я не хочу, чтобы TicketMaster смогла идентифицировать и преследовать меня.
Это похоже на четыре блока данных, разделённых двоеточиями. Сначала идут какие-то данные, закодированные Base64, за которыми следуют два шестизначных числа, а в конце указана метка времени Unix.
Когда штрих-код меняется каждые 15 секунд, его содержимое немного меняется. Данные base64 остаются статичными, меняются только шестизначные числа и метка времени.
B4cq2BdFCpFl90TDuYD3pWfRDSO6eQ3bR0YQqsDnyfciuVFkKp+m0zI+a2lgfonY::358190::038184::1707070859
По своему поведению эти шестизначные числа во многом походят на Time-based One-Time Password (TOTP), используемые в таких приложениях 2FA, как Authy и Google Authenticator. Это обновляющиеся шестизначные числа, которые можно сгенерировать из общего секрета и метки времени.
Чутьё подсказывало мне, что первые два числа и в самом деле окажутся TOTP, сгенерированными из разных секретов на основе метки времени Unix, добавленной в конец данных штрих-кода. Это логично: TicketMaster не захотела бы заново изобретать велосипед в этой системе, поэтому в качестве одной из её частей компания использовала криптографический инструмент.
Данные base64 по-прежнему оставались загадкой. После декодирования данных в составляющие их 48 байта я не смог выделить в них каких-нибудь осмысленных структур данных. Они более-менее походили на случайные данные, а поскольку они не меняются при обновлении штрих-кода, то, вероятно, это какой-то случайный токен предъявителя, идентифицирующий владельца билета и сам билет.
Когда билет сканируется в турникете, TicketMaster (или, возможно, организатор мероприятия) ищет метаданные билета, использующие этот токен предъявителя, а затем валидирует два OTP с двумя секретами, хранящимися в его базе данных. Если оба этапа проходят успешно, то билет подлинный, и охрана может вас пропустить.
Маленький секрет
TOTP можно гибко настраивать, но в общем случае отрасль разработки ПО выбрала для стандартизации TOTP определённое множество параметров. Для генерации TOTP достаточно двух частей:
- Общего секрета (просто массив байтов)
- Работающих часов
Если у вас есть и то, и другое, то вы можете генерировать любое количество TOTP, находясь полностью офлайн.
В данных штрих-кода присутствуют два TOTP, так что, вероятно, мне нужно найти два общих секрета. Если у меня будут оба плюс токен предъявителя, то я смогу генерировать любое количество валидных штрих-кодов.
То есть моя задача стала намного чётче: нужно выяснить, откуда берутся эти токены и секреты.
Отладка веб-приложения
Я включил телефон с Android и подключил его браузер Chrome к инструментам разработчика Chrome (Chrome DevTools) на десктопе. Это дало мне доступ к API и исходному коду TicketMaster.
Записывая сетевые запросы в процессе загрузки функции просмотра штрих-кодов TicketMaster, я обнаружил один особенно любопытный запрос:
POST /api/render-ticket/secure-barcode?time=1707071877481&amid=XXXXXXXXXXXXXXX&_format=json
Данные ответа на него:
{
"deviceId": "8f651107-acad-42a4-b3a6-019aaac41960",
"deviceType": "WEB",
"deviceOs": "ANDROID",
"userAgent": "Mozilla/5.0 (Linux; Android 10; K) XXXXXXXXXXXXXXX",
"nfcCapableDevice": true,
"tickets": [
{
"eventId": "myevent.50.38991943985838B9",
"section": "3",
"row": "A",
"seat": "1",
"barcode": "481848590102K",
"addedValue": false,
"generalAdmission": false,
"fan": null,
"token": "eyJiIjoiNDgxODQ4NTkwMTAySyIsInQiOiJUR1JMWUNxQWYyQ1MvQmxILzh5dThZdkhoV055TW8xUW9CYTI5UTVqVkN4V2xBcE5NbnczSlJkeU9UcFVVWUFDIiwiY2siOiJiOTg0MzJlZDIzYjhmMmJkYTgyMzQ4MjE2MjI5ZjRkMjdjZTlkMDYzIiwiZWsiOiJiMzUxOTM2NGUwYzc5MTRjMWY5ZDU5ZDM1NjUyYTA0MDY3ZDJmNjQ3IiwicnQiOiJyb3RhdGluZ19zeW1ib2xvZ3kifQ==",
"renderType": "rotating_symbology",
"passData": {
"android": {
"jwt": "eyJhbGciOiJSUzI1NiJ9.XXXXXXXXXXXXXXXXXX.YYYYYYYYYYYYYYYYYYYYYYYYYY"
}
},
"bindingRequired": true,
"deviceKeyBindingRequired": false,
"deviceSignatureRequired": false,
"segmentType": "NFC_ROTATING_SYMBOLOGY",
"ticketId": "50.3.A.1"
}
],
"globalUserId": "k39Fj4lNfOS4Zq481bxIWg"
}
Примечание: я скрэмблировал идентифицирующие данные, чтобы избежать наказания.
Обратите внимание на свойство token
объекта в массиве tickets
. Я декодировал его из base64 и обнаружил, что это ещё один объект JSON:
{
"b": "481848590102K",
"t": "TGRLYCqAf2CS/BlH/8yu8YvHhWNyMo1QoBa29Q5jVCxWlApNMnw3JRdyOTpUUYAC",
"ck": "b98432ed23b8f2bda82348216229f4d27ce9d063",
"ek": "b3519364e0c7914c1f9d59d35652a04067d2f647",
"rt": "rotating_symbology"
}
- Похоже, свойство
b
совпадает со свойствомbarcode
объекта tickets. - Свойство
rt
совпадает со свойствомrenderType
объекта tickets. - Свойство
t
— это закодированный base64 массив байтов длиной 48 байтов. - Свойства
ck
иek
— это массивы байтов в шестнадцатеричной кодировке, каждый длиной 20 байтов.
Я ещё раз отсканировал последний штрих-код, отображённый в веб-приложении TicketMaster:
TGRLYCqAf2CS/BlH/8yu8YvHhWNyMo1QoBa29Q5jVCxWlApNMnw3JRdyOTpUUYAC::492436::240860::1707074879
Отлично. То есть t
— это статичный токен предъявителя. Любопытно, будут ли ck
и ek
теми секретами TOTP, которые я ищу?
После дальнейшего изучения минифицированного исходного кода веб-сайта TicketMaster я обнаружил в файле presence-secure-entry.js
функцию generateSignedToken
, которую веб-приложение использует для генерации данных штрих-кодов.
key: "generateSignedToken",
value: function(t) {
var e = arguments.length > 1 && void 0 !== arguments[1] && arguments[1];
if (this.displayType === l.ROTATING) {
var n = [this.eventKey, this.customerKey]
, a = t;
if (this.eventKey) {
var u = new Date(a);
a = u instanceof Date && "Invalid Date" !== "".concat(u) ? u : new Date
}
var A = n.reduce((function(t, n) {
if (n) {
var u;
try {
u = i.b32encode(o.a.hexToByteString(n))
} catch (t) {
u = ""
}
var A = r.a(u, 15).now(a, e);
t.push(A)
}
return t
}
), [this.rawToken]);
if (this.eventKey) {
var s = Math.floor(a.getTime() / 1e3);
A.push(s)
}
return A.join("::")
}
return this.barcode
}
Минификация усложняет чтение кода, но похоже, что ek
и ck
расшифровываются как eventKey
и customerKey
, а токен предъявителя t
называется в приведённом выше коде rawToken
.
Похоже, два TOTP генерируются с шагом в 15 секунд, но во всём остальном создаются точно так же, как стандартные для отрасли TOTP SHA-1, которые можно встретить в любом мобильном приложении для 2FA. Первый генерируется с помощью eventKey
, а второй — с помощью customerKey
. Далее в конец добавляется метка времени Unix, использованная для генерации обоих TOTP, чтобы помочь с верификацией на стороне сервера.
Чтобы проверить свою интерпретацию, я установил инструмент командной строки для работы с TOTP oathtool
. Затем я подставлял ck
, ek
и метку времени Unix в генератор TOTP SHA-1 с интервалом в 15 секунд:
$ sudo apt install oathtool -y
...
$ date=$(python3 -c 'import datetime; print(datetime.datetime.fromtimestamp(1707074879).isoformat())')
$ oathtool --totp --time-step-size 15s -N "$date" b3519364e0c7914c1f9d59d35652a04067d2f647
492436
$ oathtool --totp --time-step-size 15s -N "$date" b98432ed23b8f2bda82348216229f4d27ce9d063
240860
Отлично! Это совпадает с двумя TOTP в штрих-коде:
TGRLYCqAf2CS/BlH/8yu8YvHhWNyMo1QoBa29Q5jVCxWlApNMnw3JRdyOTpUUYAC::492436::240860::1707074879
Вероятно, eventKey
уникален для конкретного мероприятия, на которое покупаются билеты, а customerKey
, вероятно, уникален для владельца билета. Похоже, они вообще не меняются, в отличие от rawToken
, который обновляется при каждом обновлении веб-приложения TicketMaster. Однако если оставить страницу в покое на несколько часов, rawToken
не поменяется, то есть он предположительно должен оставаться валидным даже после закрытия веб-приложения.
А что насчёт поля passData.android.jwt
? Оно на что-то влияет? Я сэкономлю вам время: оказалось, что оно вообще не нужно для верификации билета; скорее всего, это просто токен аутентификации, используемый для сохранения билета в Google Wallet пользователя. Я не пользуюсь Google Wallet, потому что сильно забочусь о конфиденциальности и стараюсь держаться как можно дальше от сервисов Google.
Пиратим билеты
Теперь я знаю всё необходимое для дублирования штрих-кодов TicketMaster в собственном приложении или даже для перепродажи билета вне огороженного маркетплейса TicketMaster. Для того мне достаточно будет извлечь зашифрованное base64 свойство token
из конечной точки API /api/render-ticket/secure-barcode
или разработать способ динамического получения этого токена при помощи учётных данных сессии TicketMaster.
Эта строка token
в кодировке base64 и есть билет, по крайней мере, для охранников на входе. Если у вас есть валидные rawToken
, eventKey
и customerKey
, то можно генерировать валидные штрих-коды PDF417, неотличимые от штрих-кодов из официального приложения TicketMaster. Не имея возможности проверять документ с фотографией на входе, сотрудники компании-организатора не смогут понять, тот ли человек находится возле турникета, что и человек, на которого зарегистрирован билет в TicketMaster.
Забавно, что TicketMaster сам упростил процесс извлечения токенов: когда на веб-странице размещается компонент рендерера штрих-кода, token
автоматически выводится в консоль браузера.
r.a.log(
"'render' called on '".concat(
"pseview-".concat(J.get(this)),
"' with token '", this.token, "'"
)
)
Это значит, что для получения token
нам даже не придётся возиться с инъецированием собственных пользовательских скриптов на страницу. Достаточно просто открыть штрих-код SafeTix в веб-приложении TicketMaster, подключить инстанс Chrome на телефоне к Chrome DevTools ноутбука и открыть консоль. В неё выведется token
. Можно скопировать его и использовать, как угодно.
Сроки жизни
Единственный неизвестный фактор здесь — это срок жизни rawToken
. Сложно узнать точно, как бэкенд-сервер TicketMaster использует rawToken
для поиска билета. Вероятно, новый rawToken
генерируется каждый раз, когда клиент обращается к конечной точке /api/render-ticket/secure-barcode
.
Я понятия не имею, как долго rawToken
остаётся валидным. Возможно, за раз для аккаунта TicketMaster может быть валидным единственный rawToken
. Разработчики TicketMaster могли спроектировать систему таким образом, чтобы предотвратить извлечение нескольких билетов, валидных одновременно.
Если одновременно валидно множество token
, то один человек мог бы купить десятки билетов, извлечь нужное ему количество token
и перепродать их «из-под полы». Было бы здорово, если бы TicketMaster не подумала об этом, потому что тогда бы я мог извлекать token
билетов для друзей и распространять их без необходимости общения с конвейером сбора данных TicketMaster.
Единственный авторитетный источник, который я смог найти по этой теме — загадочный документ на веб-сайте документации API разработчиков TicketMaster.
Партнёрам нужно обновлять токен за 20 часов до начала события и каждый раз, когда билет отображается в приложении.
FAQ
- Как часто должен обновляться токен? Нужно обновлять токен каждый раз, когда зритель открывает и просматривает билет в вашем приложении, и за 20 часов до события. Если мы не сможете обновить токен, когда зритель откроет билет на турникете, то SDK попытается использовать токен, обновлённый за 20 часов до этого. Токен по-прежнему должен быть валидным. Вам не нужно обновлять токен каждые 20 часов.
На основании этого разумно будет предположить, что rawToken
валиден только в течение 20 часов, а это должно означать, что вам нужно получить rawToken
не позже, чем за 20 часов до мероприятия, чтобы перепродать или передать его без разрешения TicketMaster. Однако если вы просто хотите сохранить билет офлайн, то это более чем адекватное решение. Я даже собрал на Expo небольшое приложение, которое я назвал TicketGimp; если передать ему token
, оно рендерит штрих-коды SafeTix.
Хочу протестировать его, когда настанет дата моего концерта.
Заключение
Думаю, все мы согласны с девизом Fuck TicketMaster. Надеюсь, её ничтожные продакт-менеджеры и высшее руководство прочитают мою статью, и у них начнётся истерика. Надеюсь, её прочитают разработчики, и испытают стыд. Я редко ощущаю искреннюю неприязнь к другим разработчикам, но проектировавшим эту систему я скажу: «Позор вам».
Позор вам за то, что вы впустую тратите свои умения на ограничение возможностей людей, не владеющих современными технологиями.
Позор вам за то, что вы позволяете команде маркетологов использовать этот тёмный паттерн как меру защиты.
Позор вам за поддержку компании с такими порочными практиками ведения бизнеса.
Разработчики ПО — волшебники и шаманы современной эпохи. Мы должны использовать свои силы осмотрительно и ответственно, что и подразумевают эти силы. А вы используете эти силы, чтобы не пускать людей на развлекательные мероприятия.
Удачи вам в рефакторинге вашей системы верификации билетов.
Автор: PatientZero