o
    >{Fe[T                     @   s   d Z ddlZddlZddlZddlZddlmZmZ ddlm	Z	 ddl
m
Z
 ddlmZ ddlmZ ddlmZ dd	lmZmZ dd
lmZ ddlmZ ddlmZ G dd deZG dd deZdd Zdd Zdd Z dd Z!dd Z"dddZ#dS )ao	  
This submodule contains various functionality related to Steam Guard.

:class:`SteamAuthenticator` provides methods for genereating codes
and enabling 2FA on a Steam account. Operations managing the authenticator
on an account require an instance of either :class:`.MobileWebAuth` or
:class:`.SteamClient`. The instance needs to be logged in.

Adding an authenticator

.. code:: python

    wa = MobileWebAuth('steamuser')
    wa.cli_login()

    sa = SteamAuthenticator(backend=wa)
    sa.add()    # SMS code will be send to the account's phone number
    sa.secrets  # dict with authenticator secrets (SAVE THEM!!)

    # save the secrets, for example to a file
    json.dump(sa.secrets, open('./mysecrets.json', 'w'))

    # HINT: You can stop here and add authenticator on your phone.
    #       The secrets will be the same, and you will be able to
    #       both use your phone and SteamAuthenticator.

    sa.finalize('SMS CODE')  # activate the authenticator
    sa.get_code()  # generate 2FA code for login
    sa.remove()  # removes the authenticator from the account

.. warning::
    Before you finalize the authenticator, make sure to save your secrets.
    Otherwise you will lose access to the account.

Once authenticator is enabled all you need is the secrets to generate codes.

.. code:: python

    secrets = json.load(open('./mysecrets.json'))

    sa = SteamAuthenticator(secrets)
    sa.get_code()

You can obtain the authenticator secrets from an Android device using
:func:`extract_secrets_from_android_rooted`. See the function docstring for
details on what is required for it to work.

Format of ``secrets.json`` file:

.. code:: json

    {
        "account_name": "<username>",               # account username
        "identity_secret": "<base64 encoded>",      # unknown
        "revocation_code": "R51234",                # revocation code
        "secret_1": "<base54 encoded>",             # unknown
        "serial_number": "1111222333344445555",     # serial number
        "server_time": "1600000000",                # creation timestamp
        "shared_secret": "<base65 encoded>",        # secret used for code generation
        "status": 1,                                # status, 1 = token active
        "token_gid": "a1a1a1a1a1a1a1a1",            # token gid
        "uri": "otpauth://totp/Steam:<username>?secret=<base32 encoded shared seceret>&issuer=Steam"
    }
    N)	b64decode	b64encode)hexlify)time)webapi)ETwoFactorTokenType)SteamID)	hmac_sha1	sha1_hash)EResult)MobileWebAuth)proto_to_dictc                   @   s   e Zd ZdZdZdZdZdZdZdZ	d*ddZ
dd Zd	d
 Zd+ddZd,ddZdd Zdd Zdd Zd+ddZdd Zd+ddZdd Zdd Zd d! Zd"d# Zd$d% Zd&d' Zd(d) ZdS )-SteamAuthenticatorzNAdd/Remove authenticator from an account. Generate 2FA and confirmation codes.   Nr   c                 C   s   |pi | _ || _dS )z
        :param secret: a dict of authenticator secrets
        :type  secret: dict
        :param backend: logged on session for steam user
        :type  backend: :class:`.MobileWebAuth`, :class:`.SteamClient`
        N)secretsbackend)selfr   r    r   6/usr/local/lib/python3.10/dist-packages/steam/guard.py__init__Z   s   

zSteamAuthenticator.__init__c                 C   s$   || j vrtdt| | j | S )NzNo %s attribute)r   AttributeErrorrepr)r   keyr   r   r   __getattr__d   s   

zSteamAuthenticator.__getattr__c                 C   sP   | j du s| jrt | j | jkrt | _ | j durt | _tt | j p%d S )zF
        :return: Steam aligned timestamp
        :rtype: int
        Nr   )steam_time_offsetalign_time_everyr   _offset_last_checkget_time_offsetintr   r   r   r   get_timei   s   

zSteamAuthenticator.get_timec                 C   s"   t t| j|du r|  S |S )z
        :param timestamp: time to use for code generation
        :type timestamp: int
        :return: two factor code
        :rtype: str
        N) generate_twofactor_code_for_timer   shared_secretr    )r   	timestampr   r   r   get_codex   s
   
zSteamAuthenticator.get_code c                 C   s$   t t| j||du r|  S |S )z
        :param tag: see :func:`generate_confirmation_key` for this value
        :type tag: str
        :param timestamp: time to use for code generation
        :type timestamp: int
        :return: trade confirmation key
        :rtype: str
        N)generate_confirmation_keyr   identity_secretr    )r   tagr#   r   r   r   get_confirmation_key   s
   	z'SteamAuthenticator.get_confirmation_keyc              
   C   s  | j }t|tr@|jstd|j|d< d|d< ztjd|d|d}W n tj	j
y9 } ztdt| d }~ww |d	 }|S |jsGtd
|jd| |dd}|d u rYtd|jjtjkrntd|jjt|jjf t|j}|dkrdD ]}t|| d||< qy|S )Nz$MobileWebAuth instance not logged inaccess_token
   http_timeoutITwoFactorService   paramszError adding via WebAPI: %sresponsez"SteamClient instance not logged inzTwoFactor.%s#1)timeoutzFailed. Request timeoutzFailed: %s (%s)AddAuthenticator)r"   r'   secret_1ascii)r   
isinstancer   	logged_onSteamAuthenticatorErroroauth_tokenr   postrequests
exceptionsRequestExceptionstrsend_um_and_waitheadereresultr   OKerror_messager   r   bodyr   decode)r   actionr0   r   respexpr   r   r   r   _send_request   s<   





z SteamAuthenticator._send_requestc              	   C   s   |   std| d| jjtt ttjt	| jjdd}|d t
jkr3tdtt
|d  || _t|d t  | _dS )	zAdd authenticator to an account.
        The account's phone number will receive a SMS code required for :meth:`finalize`.

        :raises: :class:`SteamAuthenticatorError`
        z,Account doesn't have a verified phone numberr3   1)steamidauthenticator_timeauthenticator_typedevice_identifiersms_phone_idstatusz&Failed to add authenticator. Error: %sserver_timeN)has_phone_numberr8   rI   r   steam_idr   r   r   ValveMobileAppgenerate_device_idr   rB   r   r   r   )r   rG   r   r   r   add   s   
zSteamAuthenticator.addc                 C   s   |  d| jjtt |  |d}|d tjkr7|ddr7| j	r7|  j
d7  _
|  j	d8  _	| | dS |d	 sJd
| _	tdtt|d  t|d t  | _
dS )zFinalize authenticator with received SMS code

        :param activation_code: SMS code
        :type activation_code: str
        :raises: :class:`SteamAuthenticatorError`
        FinalizeAddAuthenticator)rK   rL   authenticator_codeactivation_coderP   	want_moreF   r.   Nsuccessr   z+Failed to finalize authenticator. Error: %srQ   )rI   r   rS   r   r   r$   r   TwoFactorActivationCodeMismatchget_finalize_attemptsr   finalizer8   r   )r   rY   rG   r   r   r   r`      s    
zSteamAuthenticator.finalizec                 C   sl   | j stdt| jtstd| d| jj|r|n| jdd}|d s/td|d f | j   d	S )
a~  Remove authenticator

        :param revocation_code: revocation code for account (e.g. R12345)
        :type  revocation_code: str

        .. note::
            After removing authenticator Steam Guard will be set to email codes

        .. warning::
            Doesn't work via :class:`.SteamClient`. Disabled by Valve

        :raises: :class:`SteamAuthenticatorError`
        z#No authenticator secrets available?z Only available via MobileWebAuthRemoveAuthenticatorr.   )rK   revocation_codesteamguard_schemer\   z8Failed to remove authenticator. (attempts remaining: %s)revocation_attempts_remainingN)	r   r8   r6   r   r   rI   rS   rb   clear)r   rb   rG   r   r   r   remove   s   zSteamAuthenticator.removec                 C   s   |  dd| jjiS )zFetch authenticator status for the account

        :raises: :class:`SteamAuthenticatorError`
        :return: dict with status parameters
        :rtype: dict
        QueryStatusrK   rI   r   rS   r   r   r   r   rP     s   zSteamAuthenticator.statusc                 C   s,   |r|  dd|idg S |  di  dS )a  Generate emergency codes

        :param code: SMS code
        :type code: str
        :raises: :class:`SteamAuthenticatorError`
        :return: list of codes
        :rtype: list

        .. note::
            A confirmation code is required to generate emergency codes and this method needs
            to be called twice as shown below.

        .. code:: python

            sa.create_emergency_codes()              # request a SMS code
            sa.create_emergency_codes(code='12345')  # creates emergency codes
        createemergencycodescodecodesN)rI   r^   )r   rj   r   r   r   create_emergency_codes  s   z)SteamAuthenticator.create_emergency_codesc                 C   s   |  dd| jji dS )zWDestroy all emergency codes

        :raises: :class:`SteamAuthenticatorError`
        DestroyEmergencyCodesrK   Nrh   r   r   r   r   destroy_emergency_codes$  s   z*SteamAuthenticator.destroy_emergency_codesc                 C   sB   t | jtr
| jjS | jjr| j }|du rtd|S td)z
        :return: authenticated web session
        :rtype: :class:`requests.Session`
        :raises: :class:`RuntimeError` when session is unavailable
        Nz7Failed to get a web session. Try again in a few minutesz%SteamClient instance is not connected)r6   r   r   sessionr7   get_web_sessionRuntimeError)r   sessr   r   r   _get_web_session+  s   
z#SteamAuthenticator._get_web_sessionc              
   C   N   |   }z|jdd|dd|jjdddddd	 }W |S    d
di Y S )a  Add phone number to account

        Steps:

        1. Call :meth:`add_phone_number()` then check ``email_confirmation`` key in the response

           i. On ``True``, user needs to click link in email, then step 2
           ii. On ``False``, SMS code is sent,  go to step 3

        2. Confirm email via :meth:`confirm_email()`, SMS code is sent
        3. Finalize phone number with SMS code :meth:`confirm_phone_number(sms_code)`

        :param phone_number: phone number with country code
        :type  phone_number: :class:`str`
        :return: see example below
        :rtype: :class:`dict`

        .. code:: python

            {'success': True,
             'email_confirmation': True,
             'error_text': '',
             'fatal': False}
        /https://steamcommunity.com/steamguard/phoneajaxadd_phone_numberr   	sessionidsteamcommunity.comdomainopargcheckfortosskipvoiprw      datar2   r\   Frs   r:   cookiesr^   jsonr   phone_numberrr   rG   r   r   r   rv   >  s    z#SteamAuthenticator.add_phone_numberc              
   C   sP   |   }z|jddddd|jjddddd	d
 }W |S    ddd Y S )a  Confirm email confirmation. See :meth:`add_phone_number()`

        .. note::
            If ``email_confirmation`` is ``True``, then user hasn't clicked the link yet.

        :return: see example below
        :rtype: :class:`dict`

        .. code:: python

            {'success': True,
             'email_confirmation': True,
             'error_text': '',
             'fatal': False}
        ru   email_confirmationr%   r.   rw   rx   ry   r{   r   r   TF)fatalr\   r   r   rr   rG   r   r   r   confirm_emailh  s    z SteamAuthenticator.confirm_emailc              
   C   rt   )aW  Confirm phone number with the recieved SMS code. See :meth:`add_phone_number()`

        :param sms_code: sms code
        :type  sms_code: :class:`str`
        :return: see example below
        :rtype: :class:`dict`

        .. code:: python

            {'success': True,
             'error_text': '',
             'fatal': False}
        ru   check_sms_coder.   rw   rx   ry   r{   r   r   r\   Fr   )r   sms_coderr   rG   r   r   r   confirm_phone_number  s    z'SteamAuthenticator.confirm_phone_numberc              
   C   sN   |   }z|jddddd|jjdddd	d
d }W |S    ddi Y S )a  Check whether the account has a verified phone number

        :return: see example below
        :rtype: :class:`dict`

        .. code:: python

            {'success': True,
             'has_phone': True,
             'error_text': '',
             'fatal': False}
        ru   	has_phone0r   r.   rw   rx   ry   r{   r   r   r\   Fr   r   r   r   r   rR     s    z#SteamAuthenticator.has_phone_numberc                 C   sL   |   }z|jd||jjddddddd }W |S    d	di}Y |S )
a  Test whether phone number is valid for Steam

        :param phone_number: phone number with country code
        :type  phone_number: :class:`str`
        :return: see example below
        :rtype: :class:`dict`

        .. code:: python

            {'is_fixed': False,
             'is_valid': False,
             'is_voip': True,
             'number': '+1 123-555-1111',
             'success': True}
        z-https://store.steampowered.com/phone/validaterw   zstore.steampowered.comry   )phoneNumber	sessionIDFr   )r   allow_redirectsr2   r\   r   r   r   r   r   validate_phone_number  s   

z(SteamAuthenticator.validate_phone_number)NN)N)r%   N)__name__
__module____qualname____doc__r_   r   r   r   r   r   r   r   r    r$   r)   rI   rV   r`   rf   rP   rl   rn   rs   rv   r   r   rR   r   r   r   r   r   r   Q   s4    




%
 
	*!r   c                   @   s   e Zd ZdS )r8   N)r   r   r   r   r   r   r   r8     s    r8   c                 C   s   t | t t pd S )zGenerate Steam 2FA code for login with current time

    :param shared_secret: authenticator shared shared_secret
    :type shared_secret: bytes
    :return: steam two factor code
    :rtype: str
    r   )r!   r   r   )r"   r   r   r   generate_twofactor_code  s   r   c           	      C   s   t t| tdt|d }t|dd d@ }td|||d  d d	@ }d
}d}tdD ]}t|t	|\}}||| 7 }q1|S )a  Generate Steam 2FA code for timestamp

    :param shared_secret: authenticator shared secret
    :type shared_secret: bytes
    :param timestamp: timestamp to use, if left out uses current time
    :type timestamp: int
    :return: steam two factor code
    :rtype: str
    >Qr[         r   z>I   r   i23456789BCDFGHJKMNPQRTVWXYr%   r   )
r	   bytesstructpackr   ordunpackrangedivmodlen)	r"   r#   hmacstartcodeintcharsetrj   _ir   r   r   r!     s   
 r!   c                 C   s(   t dt||d }tt| |S )a;  Generate confirmation key for trades. Can only be used once.

    :param identity_secret: authenticator identity secret
    :type identity_secret: bytes
    :param tag: tag identifies what the request, see list below
    :type tag: str
    :param timestamp: timestamp to use for generating key
    :type timestamp: int
    :return: confirmation key
    :rtype: bytes

    Tag choices:

        * ``conf`` to load the confirmations page
        * ``details`` to load details about a trade
        * ``allow`` to confirm a trade
        * ``cancel`` to cancel a trade

    r   r5   )r   r   r   encoder	   r   )r'   r(   r#   r   r   r   r   r&     s   r&   c                  C   sN   zt jdddddid} W n   Y dS tt }t| di d	|| S )
zGet time offset from steam server time via WebAPI

    :return: time offset (``None`` when Steam WebAPI fails to respond)
    :rtype: :class:`int`, :class:`None`
    r-   	QueryTimer.   r,   r+   r/   Nr1   rQ   )r   r:   r   r   r^   )rG   tsr   r   r   r   $  s   
r   c                 C   sV   t tt| dd}d|dd |dd |dd |dd |dd f S )	zGenerate Android device id

    :param steamid: Steam ID
    :type steamid: :class:`.SteamID`, :class:`int`
    :return: android device id
    :rtype: str
    r5   zandroid:%s-%s-%s-%s-%sN         r       )r   r
   r>   r   rE   )rK   hr   r   r   rU   2  s   :rU   adbc                 C   sh   t | ddddg}|ddd }|d d	kr"td
t| dd ttj|	dddD S )aY  Extract Steam Authenticator secrets from a rooted Android device

    Prerequisite for this to work:

        - rooted android device
        - `adb binary <https://developer.android.com/studio/command-line/adb.html>`_
        - device in debug mode, connected and paired

    .. note::
        If you know how to make this work, without requiring the device to be rooted,
        please open a issue on github. Thanks

    :param adb_path: path to adb binary
    :type adb_path: str
    :raises: When there is any problem
    :return: all secrets from the device, steamid as key
    :rtype: dict
    shellsuz-czL'cat /data/data/com.valvesoftware.android.steam.community/files/Steamguard*'zutf-8
r   {zGot invalid data: %sc                 S   s   i | ]	}t |d  |qS )rK   )r   ).0xr   r   r   
<dictcomp>\  s    z7extract_secrets_from_android_rooted.<locals>.<dictcomp>z}{z}|||||{z|||||)

subprocesscheck_outputrE   splitrq   r   mapr   loadsreplace)adb_pathr   r   r   r   #extract_secrets_from_android_rooted=  s   r   )r   )$r   r   r   r   r;   base64r   r   binasciir   r   steamr   steam.enumsr   steam.steamidr   steam.core.cryptor	   r
   steam.enums.commonr   steam.webauthr   steam.utils.protor   objectr   	Exceptionr8   r   r!   r&   r   rU   r   r   r   r   r   <module>   s4    @   
