Steam auth что это

от admin

Steam auth что это

Каждого пользователя Steam можно идентифицировать с помощью уникального 64-битного Steam ID, известного как Steam ID пользователя. В API Steamworks, написанном на C++, SteamID пользователя содержится внутри объекта CSteamID. Можно возвратить SteamID текущего пользователя, вызвав ISteamUser::GetSteamID, а затем возвратить 64-битный ID, вызвав CSteamID.ConvertToUint64() на возвращённом значении.

Следующие методы аутентификации могут использоваться в целях надёжного подтверждения Steam ID пользователя.

Все API, освещённые в этом документе

Билеты сессии аутентификации
Зашифрованные билеты приложений

Зашифрованные билеты приложений можно использовать для подтверждения личности пользователя, когда его игровой клиент подключён к защищённому внутреннему серверу. В отличие от билетов сессии аутентификации, подтверждение зашифрованных билетов приложений не требует от защищённого сервера HTTPS запросов. Взамен, для подтверждения билета защищённый сервер использует библиотеку C++ и закрытый симметричный ключ. The Steamworks SDK includes 32-bit and 64-bit versions of this library for Windows and Linux under the public/steam/lib directory.

Before using Encrypted Application Tickets, you must generate a private key for each title. You can do this by navigating to Edit Steamworks Settings for your application and selecting ‘SDK Auth’ from the ‘Security’ drop-down. This key will be associated with your title’s AppID and any downloadable content for that title. In order to access this section of Steamworks, a user must have the "Manage Signing" permission for the relevant Application.

веб-API Steamworks

P2P или игровые серверы

Билеты сессии аутентификации

Аутентификация пользователя
  • Клиент А должен возвратить билет сессии аутентификации, вызвав ISteamUser::GetAuthSessionTicket.
  • Клиент A должен отправить свой билет сессии клиенту Б.
  • Клиент Б должен передать билет клиента А функции ISteamUser::BeginAuthSession, которая совершит краткую проверку данных. Если билет действителен, функция ISteamUser::BeginAuthSession перенаправит билет на сервер Steam, чтобы подтвердить, что он не был использован повторно и был выдан владельцем аккаунта клиента А. Результат этого подтверждения будет возвращён в обратном вызове ISteamUser::ValidateAuthTicketResponse_t.
  • Когда сессия многопользовательской игры закончится:
    • Клиент А должен передать дескриптор, изначально возвращённый от ISteamUser::GetAuthSessionTicket в адрес функции ISteamUser::CancelAuthTicket.
    • Клиент Б должен передать SteamID клиента А функции ISteamUser::EndAuthSession.
    • Билеты сессии аутентификации можно использовать только один раз. Функцию ISteamUser::GetAuthSessionTicket необходимо вызвать для каждого клиента в сессии многопользовательской игры, который запросит билет.
    • Если билет используется для аутентификации пользователей в одноранговой сессии многопользовательской игры, каждый игровой клиент должен подтвердить подлинность каждого второго игрового клиента в сессии.
    • После окончания работы с билетом сессии аутентификации должна быть вызвана функция ISteamUser::CancelAuthTicket для каждого дескриптора, возвращённого функцией ISteamUser::GetAuthSessionTicket.
    • Когда клиент А вызовет ISteamUser::CancelAuthTicket, клиент Б получит обратный вызов ISteamUser::ValidateAuthTicketResponse_t, заявляющий, что билет клиента А больше не действителен.
    • Когда клиент А покидает игру с клиентом Б, если вызов клиентом А функции ISteamUser::CancelAuthTicket будет обработан до вызова клиентом Б функции ISteamUser::EndAuthSession, клиент Б может получить обратный вызов ISteamUser::ValidateAuthTicketResponse_t, сообщающий, что билет был отменён. Поскольку существует взаимное соглашение о том, что клиент А покидает сессию, обратный вызов будет проигнорирован.
    • Проблемы с соединением могут помешать серверу Steam предоставить своевременный обратный вызов стороне, вызвавшей ISteamUser::BeginAuthSession. Сторона, вызвавшая ISteamUser::BeginAuthSession (клиент Б), не должна предполагать, что имеет подлинную информацию о клиенте А до получения обратного вызова, однако должна разрешить продолжение сессии многопользовательской игры.
    • Если сторона, вызвавшая ISteamUser::BeginAuthSession, получит обратный вызов ISteamUser::ValidateAuthTicketResponse_t, сообщающий, что билет клиента А недействителен, вызвавшая сторона должна отказаться от продолжения сессии многопользовательской игры с клиентом А. Если другие участники игры не откажутся играть с клиентом А, вызвавшая сторона должна покинуть сессию.
    • ISteamGameServer предоставляет аналогичные способы использования билета сессии аутентификации, чтобы подтвердить личность пользователя, когда его игровой клиент подключён к игровому серверу.
    Подтверждение владения

    Внутренний сервер

    Билеты сессии аутентификации и веб-API Steamworks

    Аутентификация пользователя
    • Клиент должен возвратить билет сессии аутентификации, вызвав ISteamUser::GetAuthSessionTicket.
    • Чтобы гарантировать действительность билета, клиент должен дождаться обратного вызова ISteamUser::GetAuthSessionTicketResponse_t.
    • Клиент должен отправить свой билет сессии на защищённый сервер.
    • Защищённый сервер должен сделать HTTPS-запрос к partner.steam-api.com и вызвать сетевой способ аутентификации ISteamUserAuth/AuthenticateUserTicket, передав билет сессии аутентификации пользователя в виде строки в формате UTF-8 в шестнадцатеричной кодировке. Обратите внимание, что для данного способа требуется ключ веб-API Steam или ключ веб-API издателя, связанный с номером приложения, чтобы запрос был передан. Дальнейшие обновления этого API будут возвращать больше информации запросившей стороне, если предоставлен ключ веб-API издателя.
    • Если билет пользователя действителен, ISteamUserAuth/AuthenticateUserTicket вернёт 64-битный SteamID пользователя.
    Подтверждение владения

    Зашифрованные билеты приложений

    Аутентификация пользователя
    • Клиент должен вызвать ISteamUser::RequestEncryptedAppTicket и дождаться результата вызова ISteamUser::EncryptedAppTicketResponse_t.
    • Затем клиент должен вызвать ISteamUser::GetEncryptedAppTicket, чтобы возвратить зашифрованный билет пользователя на защищённый сервер.
    • Используя библиотеку зашифрованных билетов приложений, защищённый сервер должен:
      • Вызвать SteamEncryptedAppTicket::BDecryptTicket, чтобы расшифровать билет пользователя
      • Вызвать SteamEncryptedAppTicket::BIsTicketForApp, чтобы проверить, совпадает ли билет с ожидаемым приложением
      • Вызвать SteamEncryptedAppTicket::GetTicketIssueTime, чтобы проверить, не истёк ли срок действия билета (срок действия определяется приложением)
      • Вызвать SteamEncryptedAppTicket::GetTicketSteamID, чтобы возвратить SteamID пользователя
      Подтверждение владения

      Аутентификация с OpenID при использовании веб-браузера

      Steam is an OpenID Provider, as described in the OpenID 2.0 specification. Inside a web browser, a third-party website can use OpenID to obtain a user’s SteamID which can be used as the login credentials for the 3rd party website, or linked to an existing account on that website.

      When using OpenID, the user begins in a web browser at the third-party website. When the user wishes to login/link their account to that website, using OpenID, the site directs the user to a login form on the Steam Community website. Once the user has entered their Steam login credentials, the user’s web browser is automatically redirected back to the 3rd party website with some additional OpenID specific data appended to the return URL. The site’s OpenID library can then use this data to verify and obtain the user’s SteamID.

      Steam provides the following images which may be used by 3rd party sites when linking to the Steam sign in page:

      sits_large_noborder.png

      sits_small.png

      Аутентификация пользователя
      • Настройте библиотеку OpenID на использование следующей ссылки в качестве ссылки конечной точки провайдера Steam: https://steamcommunity.com/openid/
      • После успешной аутентификации затребованный пользователем ID будет содержать его SteamID. Формат затребованного ID Steam: http://steamcommunity.com/openid/id/<steamid> .
      Подтверждение владения

      Примеры

      Привязка сторонних аккаунтов к аккаунтам Steam

      Third-party accounts can be linked to Steam accounts by associating a user’s SteamID with the 3rd party account.

      A user’s SteamID can be securely retrieved either in-game or through a web browser and once the initial association has occurred, you can safely allow access to the 3rd party account by merely verifying a user’s SteamID. This eliminates the need for Steam users to do any sort of secondary login to 3rd party account systems. Additionally, if new 3rd party accounts can be automatically created and linked when a new SteamID is encountered, the Steam user will never have to be aware that a secondary authentication is taking place at all. Instead, their single Steam account can grant access to all of their games, streamlining the user experience and removing potential barriers to installing and trying new games.

      Для тех, кто не хочет потерять свои скины

      Небольшая заметка о том, как мошенники могут угнать у вас вещи из инвентаря, а вы даже не заметите этого.

      В связи с недавними изменениями в системе трейда CSGO, наш маркет по CSGO работает через специальную программу, требующую генерацию API ключа Steam.
      Так что если продаете у нас — не пугайтесь, если у вас появится API ключ. Однако ответственность за защиту доступа к аккаунту все еще на вашей совести. Мошенник не должен узнать этот API ключ, поэтому следите за тем, куда вы вводите код подтверждения из Steam Guard. Пользуйтесь только проверенными сайтами и программами.

      Угон вещей с помощью API ключа Steam

      Что происходит

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

      Как это происходит?

      1) Пользователь нажимает кнопку пополнение счет на сайте

      2) Бот сайта присылает пользователю трейд с секретным кодом

      3) Мошенник, имеющий доступ к API ключу пользователя, получает информацию о трейде — проверочный код, имя бота, список вещей, которые пользователь собирается отдать настоящему боту.

      4) Мошенник, используя API ключ пользователя, отменяет трейд, который прислал бот настоящего сервиса

      5) Мошенник меняет ник своего бота и присылает пользователю трейд с таким же проверочным кодом и с таким же списком вещей.

      6) Пользователь принимает трейд, даже не замечая подмены. Процесс скорее всего полностью автоматизирован.

      Что такое API ключ?

      Это комбинация цифр и букв, которая дает доступ действиям над аккаунтом Steam. Конкретно мошенники используют его для получения информации о трейдах и для их отмены.

      Как мошенник узнает API ключ пользователя?

      С помощью заманивания пользователей на сайт с поддельной формой авторизации — пользователь вводит логин и пароль с проверочным кодом из аутентификатора и фейковый сайт моментально создает API ключ, который он в последствии использует.

      Ниже приведены примеры таких поддельных форм.


      Форма авторизации открывается в маленьком окне. Поле адреса страницы пустое. Логин и пароль не заполняются автоматически, если сохранены в браузере.

      Чуть более продвинутая версия скам формы.Форма авторизации открывается в маленьком окне. Поле адреса поддельное, сделано в виде HTML элемента. Логин и пароль не заполняются автоматически, если сохранены в браузере.

      Без действий со стороны пользователя, получить этот ключ невозможно.

      Как проверить не взломан ли мой аккаунт?

      Зайдите сюда https://steamcommunity.com/dev/apikey и проверьте сгенерирован ли у вас на аккаунте API ключ.

      Если аккаунт чист — никакого ключа там не будет.

      Если если там есть ключ и вы его не создавали САМОСТОЯТЕЛЬНО (или его не создала программа market.app) то ваш аккаунт 100% взломан.

      Еще можно попробовать продать что-то дорогое или пополнить счет с помощью SkinPay

      Перед подтверждением трейда в телефоне зайдите сюда
      http://steamcommunity.com/id/me/tradeoffers/
      Если увидите 2 трейда с одинаковыми проверочными кодами, причем один отмененный а другой нет — ваш аккаунт взломан 100%

      Как вернуть предметы которые переданы через поддельный трейд?

      Никак.
      Компенсаций от маркета за подобное не предусмотрено.

      Как «вылечить» аккаунт?

      Если вы не создавали API ключ и не знаете зачем он нужен — зайдите сюда https://steamcommunity.com/dev/apikey и немедленно удалите его, нажатием кнопки Revoke My Steam Web API Key

      Зайдите сюда https://store.steampowered.com/twofactor/manage и нажмите «Выйти на всех других устройствах»

      После этого смените пароль в Steam и обязательно следите за тем, чтобы API ключ не появился вновь.

      Отправьте ссылку на эту заметку всем своим друзьям, чтобы они не попались на это. Предупрежден — вооружен.

      Steam App Auth

      As of v3.25.0, a SteamUser can retrieve Steam app tickets.
      This is currently experimental. The API is not finalized and may change at any time.

      A typical app ticket consists of a GC token, a session header, and an app ownership ticket. All three parts are sent to game servers or peers (in P2P games) for validation.

      GC stands for «game connect» here. This is a token assigned by Steam when you connect which is assigned to your login session. These aren’t assigned to any app until you «activate» a ticket. They’re used to make tickets unique.

      This part isn’t particularly interesting. It contains your external IP and some uninteresting stuff like how many tickets you’ve retrieved this session.

      App Ownership Ticket

      This part of the ticket is signed by Steam and is valid for a longer period of time, usually a couple weeks. It proves to your peer that you own the game you’re trying to authenticate for. It can be reused many times with different GC tokens.

      It contains things like your SteamID, the ID of the app it was assigned for, your external and internal IP addresses, the times when the ticket was generated and when it expires, the licenses you own which grant you this game, any DLC you own, and a signature.

      Since this part of the ticket is signed, has an expiration date, and can be reused, there’s no need to send it to Steam for validation, so it’s validated locally.

      In addition to validating session tickets using a Steam client, you can also validate session tickets using the Steam WebAPI. To do so, you should hex-encode the entire session ticket (not just the authTicket portion) and send it to the ISteamUserAuth/AuthenticateUserTicket WebAPI method.

      • ticket — A Buffer containing the app ticket you want to parse
      • allowInvalidSignature — Optional. Pass true to get back data even if the ticket has no valid signature. Defaults to false .

      This is a static method, so you should use it like this:

      Parses an app ticket. You can either parse a full appticket (GC token + session header + ownership ticket), or just an ownership ticket.

      On success, returns an object containing these properties:

      • authTicket — A Buffer containing the part of the ticket that’s sent to Steam for validation
      • gcToken — A string containing a 64-bit number which is the ticket’s «GC token» (GC stands for «game connect»)
      • tokenGenerated — A Date object containing the time when this ticket’s GC token was generated
      • sessionExternalIP — A string containing the ticket owner’s external IP address (as reported by Steam) at time of connection
      • clientConnectionTime — Time in milliseconds the ticket owner was connected to Steam when they generated this ticket (?)
      • clientConnectionCount — Number of tickets generated by the ticket owner for this Steam connection (?)
      • version — The version of the app ownership ticket
      • steamID — The ticket owner’s SteamID, as a SteamID object
      • appID — The ID of the app this ticket authenticates
      • ownershipTicketExternalIP — A string containing the external IP address of the ticket owner as reported by Steam at the time when the ownership ticket was assigned
      • ownershipTicketInternalIP — Same as above but for their internal IP. If the ticket was generated by steam-user then this may be random
      • ownershipFlags — A number containing some (probably uninteresting) flags
      • ownershipTicketGenerated — A Date object containing the time when this ticket’s ownership ticket was assigned
      • ownershipTicketExpires — Same as above but for when the ownership ticket expires
      • licenses — An array of integers containing the package IDs for all the licenses the ticket owner owns which grant them this app
      • dlc — An array of objects, each of which contains:
        • appID — The AppID of the piece of DLC
        • licenses — An array of integers containing the package IDs for all the licenses the ticket owner owns which grant them this DLC. Seems to not work right now.
        • If allowInvalidSignature is true and the signature is missing, this will be true if the ticket is not expired!

        Returns null if unable to decode any part of the ticket.

        • ticket — A Buffer containing the encrypted app ticket you want to parse
        • encryptionKey — A Buffer or a hex string containing the encryption key for the app to which this ticket belongs

        v3.26.0 or later is required to use this function

        If you happen to be the developer of an app, you could use this to decrypt one of your encrypted app tickets. You’ll need the encrypted app ticket key for the app to which the ticket you supply belongs. This happens 100% locally, so there’s no need to be able to reach Steam to do this.

        • version — The version of the app ownership ticket
        • steamID — The ticket owner’s SteamID, as a SteamID object
        • appID — The ID of the app this ticket authenticates
        • ownershipTicketExternalIP — A string containing the external IP address of the ticket owner as reported by Steam at the time when the ownership ticket was assigned
        • ownershipTicketInternalIP — Same as above but for their internal IP. If the ticket was generated by steam-user then this may be random
        • ownershipFlags — A number containing some (probably uninteresting) flags
        • ownershipTicketGenerated — A Date object containing the time when this ticket’s ownership ticket was assigned
        • licenses — An array of integers containing the package IDs for all the licenses the ticket owner owns which grant them this app
        • dlc — An array of objects, each of which contains:
          • appID — The AppID of the piece of DLC
          • licenses — An array of integers containing the package IDs for all the licenses the ticket owner owns which grant them this DLC. Seems to not work right now.

          Returns null if the provided ticket could not be parsed or could not be verified for authenticity. If you get data returned, it is guaranteed that it has not been tampered with, provided your encryption key has not been compromised.

          To determine if a ticket is valid, you should do the following:

          1. Check that the AppID matches the AppID you expect
          2. If the user has already supplied their SteamID, make sure it matches the one in the ticket
          3. Make sure it hasn’t been generated too far in the past for your liking
          4. If you built a nonce into the ticket, make sure the userData matches what you expect

          If you want to have a relatively long grace period in which an encrypted app ticket can be used, but you also want to make sure that it wasn’t reused, you can send a nonce to the client and have them build that into their encrypted app ticket’s userData .

          • appid — The ID of the app you want a ticket for
          • callback — A function to be called when the ticket is available
            • err — An Error object on failure, or null on success
            • sessionTicket — A Buffer containing the requested session ticket

            Creates and activates a new session ticket. The returned ticket contains everything you need and should be sent verbatim to your partner (i.e. a game server or P2P client). Note that the ticket itself may not be usable if you aren’t marked as in-game for the specified app on Steam.

            Each ticket may only be used once. If you’re authenticating in a P2P scheme, you’ll need to create a new ticket for every peer.

            Once you’re done with the ticket, you should cancel it using cancelAuthSessionTickets . If you want to cancel a specific ticket and not all outstanding tickets, you can parse this ticket to get the GC Token out of it.

            • appid — The ID of the app you want an ownership ticket for
            • callback — A function to be called when the ticket is available
              • err — An Error object on failure, or null on success
              • ticket — A Buffer containing the requested ownership ticket

              Gets an app ownership ticket. You can’t use this to authenticate with game servers, but you could use this to prove to someone that you own an app. Chances are, you don’t want this. This is used internally by createAuthSessionTicket .

              If you have local file storage enabled, this will save your ownership tickets to disk (unless you set the saveAppTickets steam-user option to false ). If present and not expired, they will be supplied from disk whenever possible.

              If you acquire some DLC between when a ticket is saved to disk and when you call this method, the ticket you get back might not contain information about that DLC.

              Ownership tickets can be parsed with parseAppTicket but the result will be missing some properties that are contained in other ticket sections, since ownership tickets aren’t full app tickets.

              activateAuthSessionTickets(appid, tickets[, callback])

              • appid — The ID of the app you want to activate tickets for
              • tickets — An array of Buffer s containing tickets as returned by createAuthSessionTicket . All tickets must match the specified app ID.
              • callback — Optional. Called when Steam acknowledges the activation, although at this point the ticket hasn’t been validated.
                • err — An Error object on failure, or null on success

                Activates one or more session tickets that we received from another user. Prior to sending the ticket to Steam for validation, this first checks the signature and expiration date in the ownership ticket, and validates to make sure the AppID in the ticket matches the appid you provided.

                Provided local checks pass, this then sends the ticket to Steam for validation. You’ll receive the validation result in the authTicketValidation event.

                If you try to activate a ticket that you’ve already activated and haven’t yet canceled, then the ticket is silently ignored. If you try to activate a ticket that matches an already-existing AppID/SteamID auth session, then the previous session is automatically ended.

                cancelAuthSessionTickets(appid[, gcTokens][, callback])

                • appid — The ID of the app you want to cancel your ticket(s) for
                • gcTokens — An array of GC Tokens for the tickets you want to cancel. Omit or null to cancel all outstanding tickets.
                • callback — Optional. Called when Steam acknowledges the canceling.
                  • err — An Error object on failure, or null on success
                  • response — A response object
                    • canceledTicketCount — The count of how many session tickets were canceled in this request

                    Cancels our issued session tickets for the specified app. If you provide an array of gcTokens , then this only cancels the matching tickets and not all tickets. You should call this when you’re done with your auth session. When a ticket is canceled, any party that activated and validated your ticket will be notified that it is now canceled.

                    This doesn’t affect the app ownership ticket that was used in this app ticket. Ownership tickets cannot be canceled and only become invalidated when they expire.

                    endAuthSessions(appid[, steamIDs][, callback])

                    • appid — The ID of the app you want to end auth sessions for
                    • steamIDs — Optionally pass some SteamID objects (or strings that can parse into them) to restrict which sessions you want to end. Omit or pass null to end all active auth sessions for the app.
                    • callback — Optional. Called when Steam acknowledges the request.
                      • err — An Error object on failure, or null on success
                      • response — A response object
                        • canceledTicketCount — The count of how many sessions were ended in this request

                        Ends our auth sessions with other users. Once ended, we will no longer receive notifications when users’ auth session tickets are canceled or otherwise invalidated. If we’ve already been notified that a ticket was canceled or invalidated, the session was already automatically ended.

                        This does not cancel your own session tickets. If you’re leaving a game, you should call both endAuthSessions and cancelAuthSessionTickets .

                        Synchronously returns an array of objects which represent active session tickets. Each object in the array has these properties:

                        • appID — The ID of the app to which this ticket belongs
                        • steamID — A SteamID object representing the user to whom this ticket belongs. Might be your own SteamID if this is your own ticket.
                        • ticketCrc — A number containing the CRC of the auth ticket from this session ticket (the auth ticket is the only part that’s sent to Steam).
                        • gcToken — The GC Token contained in this ticket. Can be used to uniquely identify tickets.
                        • validated — A boolean indicating whether this ticket has been validated by Steam yet or not. If false , treat the data contained in this ticket as untrusted and possibly spoofed.
                        • status — An object containing these properties:
                          • steamID — A SteamID object containing the ticket owner’s SteamID
                          • appOwnerSteamID — A SteamID object containing the app owner’s SteamID (usually the ticket owner, but might not be if they’re using family sharing)
                          • appID — The AppID of the app for which the ticket is being validated
                          • ticketCrc — The CRC32 of the ticket in question
                          • ticketGcToken — The GC Token contained in the ticket in question
                          • state — A value from some enum I’m not sure about right now
                          • authSessionResponse — A value from the EAuthSessionResponse enum

                          Emitted when Steam sends back a validation result for an activated ticket belonging to someone else.

                          If the authSessionResponse is OK , then the ticket was successfully validated. If it’s any other value, then the auth session has been implicitly canceled and there’s no need to call endAuthSession for this session.

                          This event might be emitted multiple times with the same value, so your code should be robust enough to handle such cases. This event could possibly be emitted before the callback fires or promise resolves in activateAuthSessionTickets .

                          This might be emitted with a different authSessionResponse value even after you get back OK . For example, you might get UserNotConnectedToSteam if the user disconnects from Steam, or AuthTicketCanceled if they cancel their ticket. In such cases, the auth session is implicitly canceled and you should treat the user as though they’re now unauthenticated (usually by kicking them from the session).

                          Emitted when a ticket we created is validated by someone else. This event has the same format as authTicketValidation , except the steamID property will be the SteamID of the user who validated our ticket. This might be an individual Steam account, a gameserver account, or an invalid SteamID ( ) if the ticket was validated by the WebAPI.

                          У Steam довольно любопытный способ логина

                          image

                          Как передать пароль по Интернету? Обычно приобретается сертификат SSL, а TLS выполняет задачу безопасной перемещения пароля от клиента к серверу. Разумеется, всё не так сухо, как пытаюсь представить я, но в целом это так и подобный подход прошёл проверку временем. Однако так было не всегда, и один невероятно популярный онлайн-магазин предпочёл добавить к этому процессу что-то своё. В этой статье я расскажу об уникальном способе входа в систему пользователей Steam и исследую глубокую кроличью нору удивительных подробностей его реализации.

                          Выявляем очевидное

                          Странно думать, что когда-то это было проблемой

                          Однако в начале 2010-х, не говоря уже о более давнем времени, Интернет был иным. Теперь у нас есть сервисы наподобие Let’s Encrypt, выдающие бесплатные сертификаты SSL с трёхмесячным сроком действия и возможностью автоматического обновления. Тогда почти не было иных вариантов, кроме приобретения сертификата SSL за деньги, но обычно при этом можно было получить более длительные сроки действия и поддержку. Конечно, вы можете сказать, что за безопасность и приватность пользователей стоит платить, но это не мешает появляться вопросам наподобие приведённого выше.

                          Итак, мы пришли к понимаю, что TLS важен, а теперь давайте его заменим. Представим, что мы не можем передавать пароли через HTTPS и нам каким-то образом нужно реализовать это на чистом HTTP, обеспечив при этом какой-то уровень безопасности. Существует стандартизованный и широко применяемый заголовок Authorization . Однако, в сочетании со схемой аутентификации HTTP «Basic» при использовании с чистым HTTP он не обеспечивает никакой защиты.

                          Есть проверенные и протестированные алгоритмы запроса-ответа, наиболее примечательным из которых является SRP, предназначенный для парольной аутентификации без передачи пароля, но их, вероятно, придётся реализовывать самостоятельно, и даже небольшой недосмотр может привести к серьёзному ущербу. Также можно поручить аутентификацию внешнему сервису. Схема «вход при помощи сервиса XYZ» широко распространена, но она связана с определёнными последствиями. С учётом всего этого, можно сказать, что передача секретов по не предназначенному для безопасности соединению — это нетривиальная задача.

                          Поэтому когда мы с другом решили изучить Steam в поисках следов информации, позволяющей идентифицировать людей, меня удивило то, что для обеспечения безопасности страница логина Steam использует только TLS.

                          Крипто-вишенка на торте

                          Снова запустим любимый прокси перехвата трафика и перейдём на страницу логина Steam. Введём имя пользователя и пароль, после чего нас попросят (по крайней мере, должны) ввести одноразовый токен, сгенерированный выбранным вами способом двухфакторной аутентификации. На этом можно остановиться, потому что магия, которую я хочу продемонстрировать, уже произошла. Вы заметите, что при нажатии кнопки логина запускается запрос странной конечной точки: /login/getrsakey , за которой следует /login/dologin .

                          Последовательность всех соответствующих ресурсов и запросов

                          Если изучить запрос к /login/getrsakey , то мы обнаружим ответ в формате JSON, содержащий поля с названиями, хорошо знакомыми всем, кто хотя бы немного знает о криптографии с открытым ключом. Нам передаётся открытый ключ RSA, хотя конкретные значения могут выглядеть немного странно. Очевидно, что publickey_mod и publickey_exp определяют используемые в шифровании модуль и экспонента, однако первый задаётся в шестнадцатеричном виде, а вторая — в двоичном (я вернусь к этому позже). Есть также метка времени, начальную точку которой можно определить не сразу. С назначением token_gid я пока не разобрался.

                          Страница логина при загрузке подгружает скрипты. В совершенно необфусцированном login.js содержится основной обработчик логина, поэтому любой может просто проанализировать его и разобраться, что он делает. Кроме того, сайт загружает дополнительные зависимости, в частности, jsbn.js и rsa.js .

                          Поиск по имени, указанному в первой строке jsbn.js , позволил определить, что эти два скрипта написаны выпускником MIT и Стэнфорда Томом Ву, любящим проектирование ПО и компьютерную криптографию. Он выпустил jsbn.js и rsa.js как реализацию на чистом JavaScript целых чисел с произвольной точностью и шифрования/дешифрования RSA. Также мы можем выяснить, что эти библиотеки в последний раз обновлялись в 2005 и 2013 годах, но к этому я вернусь позже. Пока просто запомним это.

                          Спускаемся в кроличью нору

                          Итак, у нас есть все нужные ресурсы, и мы можем углубиться в login.js . Его код довольно хаотичен, со множеством обратных вызовов и вызовов прокси-функций, но самые интересные части можно найти довольно быстро. По сути, скрипт можно свести к паре шагов. Каждый из шагов предполагает, что на предыдущем всё прошло правильно.

                          1. Пользователь вводит своё имя пользователя и пароль, а затем нажимает кнопку логина.
                          2. Вызывается DoLogin , который проверяет, правильно ли заполнена маска логина и выполняет запрос к /login/getrsakey .
                          3. Вызывается OnRSAKeyResponse . Он проверяет, правильно ли сформирован запрос.
                          4. Вызывается GetAuthCode . Он выполняет какой-то платформно-зависимый код в случае, если для аккаунта пользователя включены способы двухфакторной авторизации.
                          5. Вызывается OnAuthCodeResponse . Здесь пароль шифруется RSA, подготавливается и выполняется запрос к /login/dologin .
                          6. Вызывается OnLoginResponse . Пользователь выполняет вход и перенаправляется в магазин Steam.

                          Я скопировал файлы исходников на локальную машину, чтобы подробнее изучить библиотеку RSA. Модуль и экспонента передаются функции RSAPublicKey , которая ведёт себя как конструктор в «доклассовой» эпохе JavaScript. RSAPublicKey просто оборачивает значения в экземпляры BigInteger предоставленные скриптом jsbn.js . К моему удивлению, экспонента представлена не в двоичном виде, а как и модуль, в шестнадцатеричном. (Кроме того, выяснилось, что 0x010001 — это очень популярная экспонента шифрования в реализациях RSA.) То есть теперь понятно, что шифрование паролей основано на 2048-битном RSA с экспонентой шифрования, равной 65537.

                          Перейдём к полю timestamp . Ответ /login/getrsakey содержит заголовок Expires . Он ссылается на дату в прошлом, то есть ответ совершенно не должен каким-то образом кэшироваться или сохраняться. Если следить за /login/getrsakey дольше, то мы заметим, что открытый ключ постоянно часто меняется, как и метка времени. Это означает, что есть ограниченное окно времени, в течение которого конкретный открытый ключ RSA, выданный Steam, может использоваться для аутентификации.

                          Это становится ещё очевиднее при изучении последующего запроса к /login/dologin . Среди всего остального он содержит имя пользователя, зашифрованный пароль, а также метку времени выданного открытого ключа RSA. Попытка выполнить логин при изменении метки времени, как и ожидалось, оканчивается неудачей. Но важнее то, что невозможно повторно использовать старый открытый ключ даже при правильном шифровании пароля.

                          Я сделал ещё один шаг и написал простой скрипт на Python для сбора открытых ключей одноразового аккаунта на протяжении трёх дней. При помощи cronjob я запускал его каждые пять минут. Я хотел проверить, как часто меняется открытый ключ Steam и если удастся, понять, как ведёт себя поле timestamp .

                          Целая куча открытых ключей

                          Я выяснил, что открытый ключ меняется через каждые 12 попыток ввода, то есть можно логично предположить, что они заменяются каждый час. Экспонент шифрования остаётся тем же, здесь ничего неожиданного. Однако более интригующим оказалось вышеупомянутое поле timestamp . Для каждых 12 открытых ключей значение timestamp увеличивается на определённую величину, а именно на 3600000000. Кроме того, как видно на изображении выше, это число спустя определённое время зацикливается. Предупреждаю, дальнейшие рассуждения полны неподтверждённых догадок.

                          Поле timestamp зацикливается

                          Я выяснил, что 3600000000 микросекунд равно одному часу, поэтому предположил, что значение поля timestamp на самом деле задаётся в микросекундах. Однако я уже говорил, что значение метки времени с каждым новым открытым ключом не увеличивается ровно на час. На основании собранных данных я заметил, что разница между двумя последовательными метками времени равна одному часу плюс 1-2,6 секунд, и большинство из них имеет порядок 1,05-1,25 секунд. Но в таком случае возникает ещё одна интересная возможность.

                          Предположим, что новый открытый ключ генерируется каждый час плюс одна секунда. Если выполнять запрос к конечной точке ровно каждые пять минут (пока полностью игнорируя сетевую задержку), то есть вероятность, что я встречу один и тот же открытый ключ не 12, а 13 раз подряд. Это должно произойти, когда запрос совпадает по времени с генерацией нового открытого ключа. К счастью, поскольку это время порядка секунды, допуск на ошибку не так уж мал.

                          Разными цветами показаны уникальные открытые ключи (без соблюдения масштаба!)

                          Изучив мой собственный набор открытых ключей, я выяснил, что не сталкивался ни с одним таким пограничным случаем. Возможно, мне просто не повезло, или дело в том, что я рассуждал гипотетически, ожидая какого-нибудь откровения. Кроме того, при увеличении колебаний значений меток времени становится сложнее прогнозировать конкретный момент возможности наблюдения пограничного случая — если, конечно, такая ситуация вообще возникает.

                          Но помните, эта разница в час и одну-две секунды находится между двумя уникальными открытыми ключами. Вернёмся к допущению о том, что новый открытый ключ создаётся через каждый час и секунду. Тогда после 3600 публичных ключей все эти дополнительные секунды суммируются в полный час, что приведёт к описанному в предыдущем параграфе пограничному случаю. Если разница времени происходит между полным часом на часах и меткой времени открытого ключа, то всё становится понятным и эти дополнительные секунды связаны с задержкой сети. Однако для собранных мной данных это не так, поэтому ситуация озадачивает, если не сказать больше.

                          Итак, подведём итог: если все мои допущения были верны, то поле timestamp и его временная разница между открытыми ключами невероятно загадочны. Нужна ли она для учёта високосных лет? Или для компенсации какой-то другой задержки? Может, это просто ошибка реализации, которую Valve оставила? Возможно, она выражена не в микросекундах, а в чём-то более произвольном? Может, она нужна просто для того, чтобы поломали голову любопытные нерды вроде меня? Я склоняюсь к последней версии.

                          Я понимаю, что пропустил странное зацикливание значения поля timestamp и даже не касался назначения поля token_gid . Думаю, что первое нужно из-за какого-то технического ограничения, а последнее — предположительно, какой-то способ защиты от CSRF (межсайтовой подделки запроса) или уникальный идентификатор. Это совершенно необоснованные догадки, потому что я и так уже выяснил из этого исследования больше, чем ожидал. Если хотите изучить вопрос самостоятельно и поделиться своими находками, буду рад, если вы свяжетесь со мной, или по электронной почте, или в Twitter.

                          Ещё один достойный упоминания аспект: при запросе конечной точки открытого ключа с разными именами пользователей получаешь разные ответы. Непонятно, или открытые ключи берутся из пула и каждому пользователю присваивается своё смещение метки времени, или они на самом деле генерируются на лету. Кроме того, в запросе к /login/getrsakey можно использовать любое произвольное имя пользователя. Оно не обязательно должно быть зарегистрировано в Steam. Можете использовать эту информацию по своему усмотрению.

                          Хорошо, но что это значит?

                          В процессе исследования этой темы во мне зародилась странная любовь к механизму логина Steam. Теперь я знаю, что поверх использования TLS (который и нужно применять) при выполнении входа пользователя Steam также использует 2048-битный RSA для шифрования паролей пользователей при помощи системы чередующихся открытых ключей, которая корректно признаёт недействительными старые ключи и для каждого пользователя действует по-своему. Все эти труды кажутся очень избыточными, потому что для защищённого логина пользователей вполне достаточно сертификата SSL.

                          Поэтому возникает вопрос: зачем заморачиваться созданием такой странно замысловатой системы поверх механизма, вполне достаточного самого по себе? У меня есть теория, но помните — это всего лишь догадка.

                          Помните даты выпуска библиотек BigInteger и RSA? Кроме того, страница логина по-прежнему использует в качестве источника jQuery версии 1.8.3, выпущенный в ноябре 2012 года. Всё это указывает на простой факт — механизм логина практически не менялся в течение почти десятка лет. А как я сказал в начале этого поста, в те времена Интернет был совершенно иным.

                          О! Я нашёл опечатку в changelog jQuery 1.8.3! Есть ли какая-нибудь программа вознаграждений за поиск грамматических ошибок?

                          В современном вебе развивается концепция «HTTPS повсюду», однако современная ситуация стала результатом долгого и мучительного процесса. Моя теория заключается в том, что в былые времена так Steam обеспечивала слой безопасности пользователей, которые случайно или намеренно не попали на SSL/TLS-версию сайта логина. Благодаря этому даже если третья сторона сможет проанализировать все данные, передаваемые между пользователем и серверами Steam, она, по крайней мере, не сможет узнать его пароль (без мощных вычислительных ресурсов).

                          Я попытался связаться с сотрудником Valve, который точно работает над магазином Steam. Я кратко изложил ему свой анализ и теорию. Я спросил его, может ли он подтвердить это, или, может быть, знает кого-нибудь, работавшего в компании во время создания этого способа логина. Разумеется, я знаю, что у сотрудников Valve есть дела поважнее, чем отвечать на несрочную и неделовую просьбу какого-то нерда. На момент написания статьи я по-прежнему ожидаю ответа, поэтому могу только предложить свою собственную обоснованную догадку. Как бы то ни было, это исследование оказалось очень, действительно очень интересным. Исследовано ещё не всё и на этом я не закончу.

                          Читать:
                          Как поставить дроби в порядке возрастания

Похожие статьи