
    FijF                         d Z ddlZddlZddlZddlZddlZddlZddl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Zd	Zd
ZdZd
ZdZdZ edd          Zde	deddfdZ G d d          ZdS )a  
DM Pairing System

Code-based approval flow for authorizing new users on messaging platforms.
Instead of static allowlists with user IDs, unknown users receive a one-time
pairing code that the bot owner approves via the CLI.

Security features (based on OWASP + NIST SP 800-63-4 guidance):
  - 8-char codes from 32-char unambiguous alphabet (no 0/O/1/I)
  - Cryptographic randomness via secrets.choice()
  - 1-hour code expiry
  - Max 3 pending codes per platform
  - Rate limiting: 1 request per user per 10 minutes
  - Lockout after 5 failed approval attempts (1 hour)
  - File permissions: chmod 0600 on all data files
  - Codes are never logged to stdout

Storage: ~/.hermes/pairing/
    N)Path)Optional)expand_whatsapp_aliasesnormalize_whatsapp_identifier)get_hermes_dir)atomic_replace ABCDEFGHJKLMNPQRSTUVWXYZ23456789   i  iX        zplatforms/pairingpairingpathdatareturnc                 j   | j                             dd           t          j        t	          | j                   d          \  }}	 t          j        |dd          5 }|                    |           |                                 t          j	        |
                                           ddd           n# 1 swxY w Y   t          ||            	 t          j        | d	           dS # t          $ r Y dS w xY w# t          $ r( 	 t          j        |           n# t          $ r Y nw xY w w xY w)
u   Write data to file with restrictive permissions (owner read/write only).

    Uses a temp-file + atomic rename so readers always see either the old
    complete file or the new one — never a partial write.
    Tparentsexist_okz.tmp)dirsuffixwutf-8encodingNi  )parentmkdirtempfilemkstempstrosfdopenwriteflushfsyncfilenor   chmodOSErrorBaseExceptionunlink)r   r   fdtmp_pathfs        4/home/ubuntu/.hermes/hermes-agent/gateway/pairing.py_secure_writer.   7   s    	KdT222#DK(8(8HHHLBYr3111 	!QGGDMMMGGIIIHQXXZZ   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	x&&&	HT5!!!!! 	 	 	DD	   	Ih 	 	 	D	ss   	D   AB<0D  <C  D  C D  C/ /
C=9D  <C==D   
D2D D2 
D-*D2,D--D2c            
           e Zd ZdZd ZdedefdZdedefdZdefdZ	dede
fd	Zded
e
ddfdZdededefdZdededee         fdZdedededefdZdededefdZd'dedefdZd(dedededdfdZdededefdZedededefd            Z	 d(dedededee         fdZdededee
         fdZd'dedefdZd'dedefdZdededefd Z dededdfd!Z!dedefd"Z"deddfd#Z#deddfd$Z$d%edefd&Z%dS ))PairingStorea  
    Manages pairing codes and approved user lists.

    Data files per platform:
      - {platform}-pending.json   : pending pairing requests
      - {platform}-approved.json  : approved (paired) users
      - _rate_limits.json         : rate limit tracking
    c                 n    t                               dd           t          j                    | _        d S )NTr   )PAIRING_DIRr   	threadingRLock_lockselfs    r-   __init__zPairingStore.__init__[   s0    $666 _&&


    platformr   c                     t           | dz  S )Nz-pending.jsonr2   r7   r:   s     r-   _pending_pathzPairingStore._pending_patha   s    77777r9   c                     t           | dz  S )Nz-approved.jsonr<   r=   s     r-   _approved_pathzPairingStore._approved_pathd   s    88888r9   c                     t           dz  S )Nz_rate_limits.jsonr<   r6   s    r-   _rate_limit_pathzPairingStore._rate_limit_pathg   s    000r9   r   c                     |                                 rG	 t          j        |                    d                    S # t          j        t
          f$ r i cY S w xY wi S )Nr   r   )existsjsonloads	read_textJSONDecodeErrorr'   )r7   r   s     r-   
_load_jsonzPairingStore._load_jsonj   sf    ;;== 	z$..'."B"BCCC('2   				s   '> AAr   Nc                 P    t          |t          j        |dd                     d S )N   F)indentensure_ascii)r.   rE   dumps)r7   r   r   s      r-   
_save_jsonzPairingStore._save_jsonr   s)    dDJtAEJJJKKKKKr9   user_idc                 z    t          |pd                                          }|dk    rt          |          p|S |S )z<Normalize platform-specific user IDs before persisting them. whatsapp)r   stripr   )r7   r:   rP   raw_user_ids       r-   _normalize_user_idzPairingStore._normalize_user_idu   sC    '-R((..00z!!0==LLr9   c                    t          |pd                                          }|st                      S ||                     ||          h}|dk    r"|                    t          |                     |                    d           |S )z@Return all known equivalent user IDs for auth/rate-limit checks.rR   rS   )r   rT   setrV   updater   discard)r7   r:   rP   rU   aliasess        r-   _user_id_aliaseszPairingStore._user_id_aliases|   s    '-R((..00 	55L 7 7+ N NOz!!NN2;??@@@r9   leftrightc                     |                      ||          }|                      ||          }t          |o|o||z            S )z;Return True when two user IDs represent the same principal.)r\   bool)r7   r:   r]   r^   left_aliasesright_aliasess         r-   _user_ids_matchzPairingStore._user_ids_match   sI    ,,Xt<<--h>>LU]U}8TVVVr9   c                     |                      |                     |                    }|D ]}|                     |||          r dS dS )z3Check if a user is approved (paired) on a platform.TF)rI   r@   rc   )r7   r:   rP   approvedapproved_user_ids        r-   is_approvedzPairingStore.is_approved   s]    ??4#6#6x#@#@AA ( 	 	##H.>HH ttur9   c                     g }|r|gn|                      d          }|D ]^}|                     |                     |                    }|                                D ]\  }}|                    ||d|            _|S )z5List approved users, optionally filtered by platform.re   )r:   rP   )_all_platformsrI   r@   itemsappend)r7   r:   results	platformspre   uidinfos           r-   list_approvedzPairingStore.list_approved   s    "*OXJJ0C0CJ0O0O	 	H 	HAt':':1'='=>>H%^^-- H H	TA#FFFGGGGHr9   rR   	user_namec                 <                                                                   }                     |           fd|D             }|D ]}||= |t          j                    d|<                                                       |           dS )zAAdd a user to the approved list. Must be called under self._lock.c                 B    g | ]}                     |          |S  rc   ).0rf   normalized_user_idr:   r7   s     r-   
<listcomp>z.PairingStore._approve_user.<locals>.<listcomp>   sC     
 
 
 ##H.>@RSS

 
 
r9   )rr   approved_atN)rI   r@   rV   timerO   )r7   r:   rP   rr   re   duplicate_idsrf   rx   s   ``     @r-   _approve_userzPairingStore._approve_user   s    ??4#6#6x#@#@AA!44XwGG
 
 
 
 
 
$,
 
 

 !. 	+ 	+)** #9;;(
 (
#$ 	++H55x@@@@@r9   c                                                     } j        5                       |          } fd|D             }|r,|D ]}||=                      ||           	 ddd           dS 	 ddd           n# 1 swxY w Y   dS )z<Remove a user from the approved list. Returns True if found.c                 B    g | ]}                     |          |S ru   rv   )rw   rf   r:   r7   rP   s     r-   ry   z'PairingStore.revoke.<locals>.<listcomp>   sB       $''2BGLL   r9   NTF)r@   r5   rI   rO   )r7   r:   rP   r   re   matching_idsrf   s   ```    r-   revokezPairingStore.revoke   s)   ""8,,Z 	 	t,,H     (0  L
  (4 3 3$ !122h///	 	 	 	 	 	 	 		 	 	 	 	 	 	 	 	 	 	 	 	 	 	 us   ABBBcodesaltc                 z    t          j        ||                     d          z                                             S )z6Hash a pairing code with the given salt using SHA-256.r   )hashlibsha256encode	hexdigest)r   r   s     r-   
_hash_codezPairingStore._hash_code   s1     ~dT[[%9%99::DDFFFr9   c                    | j         5  |                     |           |                     ||          }|                     |          r	 ddd           dS |                     ||          r	 ddd           dS |                     |                     |                    }t          |          t          k    r	 ddd           dS d	                    d t          t                    D                       }t          j        d          }|                     ||          }t          j        d          }	||                                ||t%          j                    d||	<   |                     |                     |          |           |                     ||           |cddd           S # 1 swxY w Y   dS )a  
        Generate a pairing code for a new user.

        Returns the code string, or None if:
          - User is rate-limited (too recent request)
          - Max pending codes reached for this platform
          - User/platform is in lockout due to failed attempts

        The code is NOT stored in plaintext.  Only a salted SHA-256 hash is
        persisted so that reading the pending file does not reveal codes.
        NrR   c              3   H   K   | ]}t          j        t                    V  d S N)secretschoiceALPHABET)rw   _s     r-   	<genexpr>z-PairingStore.generate_code.<locals>.<genexpr>   s,      PP7>(33PPPPPPr9      r
   )hashr   rP   rr   
created_at)r5   _cleanup_expiredrV   _is_locked_out_is_rate_limitedrI   r>   lenMAX_PENDING_PER_PLATFORMjoinrangeCODE_LENGTHr    urandomr   r   	token_hexhexr{   rO   _record_rate_limit)
r7   r:   rP   rr   rx   pendingr   r   	code_hashentry_ids
             r-   generate_codezPairingStore.generate_code   sN    Z (	 (	!!(+++!%!8!87!K!K ""8,, (	 (	 (	 (	 (	 (	 (	 (	 $$Xw77 (	 (	 (	 (	 (	 (	 (	 (	 ood&8&8&B&BCCG7||777(	 (	 (	 (	 (	 (	 (	 (	$ 77PPU;=O=OPPPPPD :b>>Dd33I (++H "

-&"ikk! !GH OOD..x88'BBB ##Hg666Q(	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	 (	s&   AF3F3;AF3	CF33F7:F7c           	         | j         5  |                     |           |                                                                }|                     |          r	 ddd           dS |                     |                     |                    }d}d}|                                D ]\  }}t          |t                    sd|vsd|vr$	 t                              |d                   }n# t          $ r Y Rw xY w|                     ||          }	t          j        |	|d                   r|}|} n|#|                     |           	 ddd           dS ||= |                     |                     |          |           |                     ||d         |                    dd                     |d         |                    dd          dcddd           S # 1 swxY w Y   dS )u  
        Approve a pairing code. Adds the user to the approved list.

        Returns ``{user_id, user_name}`` on success, ``None`` if the code is
        invalid/expired OR the platform is currently locked out after
        ``MAX_FAILED_ATTEMPTS`` failed approvals (#10195). Callers can
        disambiguate with ``_is_locked_out(platform)``.

        Verification: the user-provided code is hashed with each stored
        entry's salt and compared to the stored hash using constant-time
        comparison. Pre-hash entries (legacy plaintext-key format from
        pre-upgrade pending.json files) are silently ignored — they get
        pruned at TTL by ``_cleanup_expired``.
        Nr   r   rP   rr   rR   )rP   rr   )r5   r   upperrT   r   rI   r>   rj   
isinstancedictbytesfromhex
ValueErrorr   r   compare_digest_record_failed_attemptrO   r}   get)
r7   r:   r   r   matched_keymatched_entryr   entryr   candidate_hashs
             r-   approve_codezPairingStore.approve_code  s    Z 3	 3	!!(+++::<<%%''D ""8,, 3	 3	 3	 3	 3	 3	 3	 3	 ood&8&8&B&BCCG K M#*==??  %!%.. &&&*=*= ==v77DD!   H!%t!<!<).%-HH "*K$)ME
 "++H555O3	 3	 3	 3	 3	 3	 3	 3	R $OOD..x88'BBB xy)A,00bAAC C C )3*..{B?? a3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	 3	sE   AG"'A$G" C-,G"-
C:7G"9C::AG"A;G""G&)G&c                    g }| j         5  |r|gn|                     d          }|D ]A}|                     |           |                     |                     |                    }|                                D ]\  }}t          |t                    s|                    d          }t          |t          t          f          sMt          t          j                    |z
  dz            }	|                    d          }
t          |
t                    r
|
dd         nd}|                    |||                    dd	          |                    d
d	          |	d           C	 ddd           n# 1 swxY w Y   |S )u  List pending pairing requests, optionally filtered by platform.

        Codes are stored hashed — the ``code`` field is replaced with the
        first 8 hex characters of the hash so admins can distinguish entries
        without revealing the original code. Legacy plaintext-key entries
        (pre-hash format) are shown with a "legacy" placeholder so admins
        can see them age out without crashing on a missing ``hash`` field.
        r   r   <   r   Nr
   legacyrP   rR   rr   )r:   r   rP   rr   age_minutes)r5   ri   r   rI   r>   rj   r   r   r   intfloatr{   r   rk   )r7   r:   rl   rm   rn   r   r   rp   r   age_minhash_valcode_displays               r-   list_pendingzPairingStore.list_pendingH  s    Z 	 	&.R

D4G4G	4R4RI  %%a(((//$*<*<Q*?*?@@&-mmoo  NHd%dD11 ! !%,!7!7J%j3,?? ! !49;;#;r"ABBG#xx//H3=h3L3L#Z8BQB<<RZLNN$% ,#'88Ir#:#:%)XXk2%>%>'.$ $    	 	 	 	 	 	 	 	 	 	 	 	 	 	 	* s   E E77E;>E;c                 N   | j         5  d}|r|gn|                     d          }|D ]e}|                     |                     |                    }|t	          |          z  }|                     |                     |          i            f	 ddd           n# 1 swxY w Y   |S )z2Clear all pending requests. Returns count removed.r   r   N)r5   ri   rI   r>   r   rO   )r7   r:   countrm   rn   r   s         r-   clear_pendingzPairingStore.clear_pendingi  s    Z 	; 	;E&.R

D4G4G	4R4RI ; ;//$*<*<Q*?*?@@W% 2 21 5 5r::::;	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; s   BBB!Bc                    |                      |                                           }|                     ||          D ]A}| d| }|                    |d          }t	          j                    |z
  t
          k     r dS BdS )z2Check if a user has requested a code too recently.:r   TF)rI   rB   r\   r   r{   RATE_LIMIT_SECONDS)r7   r:   rP   limitsaliaskeylast_requests          r-   r   zPairingStore._is_rate_limitedv  s    !6!6!8!899**8W== 	 	E''''C!::c1--L	l*.@@@tt Aur9   c                    |                      |                                           }t          j                    }|                     ||          D ]}| d| }|||<   |                     |                                 |           dS )z7Record the time of a pairing request for rate limiting.r   N)rI   rB   r{   r\   rO   )r7   r:   rP   r   nowr   r   s          r-   r   zPairingStore._record_rate_limit  s    !6!6!8!899ikk**8W== 	 	E''''CF3KK--//88888r9   c                     |                      |                                           }d| }|                    |d          }t          j                    |k     S )zBCheck if a platform is in lockout due to failed approval attempts.	_lockout:r   )rI   rB   r   r{   )r7   r:   r   lockout_keylockout_untils        r-   r   zPairingStore._is_locked_out  sP    !6!6!8!899,(,,

;22y{{]**r9   c           	         |                      |                                           }d| }|                    |d          dz   }|||<   |t          k    rMd| }t	          j                    t
          z   ||<   d||<   t          d| dt
           dt           dd	
           |                     |                                 |           dS )zMRecord a failed approval attempt. Triggers lockout after MAX_FAILED_ATTEMPTS.z
_failures:r      r   z[pairing] Platform z locked out for zs after z failed attemptsT)r#   N)rI   rB   r   MAX_FAILED_ATTEMPTSr{   LOCKOUT_SECONDSprintrO   )r7   r:   r   fail_keyfailsr   s         r-   r   z#PairingStore._record_failed_attempt  s   !6!6!8!899***

8Q''!+ x'''0h00K"&)++"?F; F8 A A A/ A A.A A AHLN N N N--//88888r9   c                 &   |                      |          }|                     |          }t          j                    }g }|                                D ]\  }}t	          |t
                    s|                    |           0|                    d          }t	          |t          t          f          s|                    |           w||z
  t          k    r|                    |           |r |D ]}||= |                     ||           dS dS )u   Remove expired pending codes.

        Tolerant of malformed / legacy entries — anything without a numeric
        ``created_at`` is treated as expired (it's effectively unusable
        with the new hash-keyed schema anyway).
        r   N)r>   rI   r{   rj   r   r   rk   r   r   r   CODE_TTL_SECONDSrO   )	r7   r:   r   r   r   expiredr   rp   r   s	            r-   r   zPairingStore._cleanup_expired  s     !!(++//$''ikk%mmoo 		) 		)NHddD)) x(((,//Jj3,77 x(((j $444x((( 	+# & &H%%OOD'*****	+ 	+r9   r   c                    g }t                                           D ]i}|j                            d| d          rI|j                            d| dd          }|                    d          s|                    |           j|S )z:List all platforms that have data files of a given suffix.-z.jsonrR   r   )r2   iterdirnameendswithreplace
startswithrk   )r7   r   rm   r,   r:   s        r-   ri   zPairingStore._all_platforms  s    	$$&& 	/ 	/Av0600011 /6>>*;f*;*;*;R@@**3// /$$X...r9   r   )rR   )&__name__
__module____qualname____doc__r8   r   r   r>   r@   rB   r   rI   rO   rV   rX   r\   r`   rc   rg   listrq   r}   r   staticmethodr   r   r   r   r   r   r   r   r   r   r   r   r   ri   ru   r9   r-   r0   r0   Q   s        ' ' '8c 8d 8 8 8 89s 9t 9 9 9 91$ 1 1 1 1t     Lt L4 LD L L L L3      
 
s 
s3x 
 
 
 
W W3 Ws Wt W W W WC # $     c T    A Ac AC AC AQU A A A A$s S T    & G GE Gc G G G \G
 =?6 66&)6696	#6 6 6 6pBS B B B B B BH S D    B	 	c 	S 	 	 	 	 s t    93 9 9 9 9 9 9+s +t + + + +9s 9t 9 9 9 9 + + + + + +4S T      r9   r0   )r   r   rE   r    r   r   r3   r{   pathlibr   typingr   gateway.whatsapp_identityr   r   hermes_constantsr   utilsr   r   r   r   r   r   r   r   r2   r   r.   r0   ru   r9   r-   <module>r      si   (   				                           , + + + + +             .      n0)<< C D    4q q q q q q q q q qr9   