
    
BjX/                         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mZ ddl	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y)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)get_hermes_dir)atomic_replace ABCDEFGHJKLMNPQRSTUVWXYZ23456789   i  iX        zplatforms/pairingpairingpathdatareturnc                 F   | j                   j                  dd       t        j                  t	        | j                         d      \  }}	 t        j                  |dd      5 }|j                  |       |j                          t        j                  |j                                ddd       t        ||        	 t        j                  | d	       y# 1 sw Y   -xY w# t        $ r Y yw xY w# t        $ r' 	 t        j                  |        # t        $ r Y  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,   2   s     	KKdT2##DKK(8HLBYYr31 	!QGGDMGGIHHQXXZ 	! 	x&	HHT5!	! 	!  		 	IIh 	  		sg   C0 $AC)C0 >C! CC0 !	C-*C0 ,C--C0 0	D :DD 	DD DD 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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	 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y)!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                 d    t         j                  dd       t        j                         | _        y )NTr   )PAIRING_DIRr   	threadingRLock_lockselfs    r+   __init__zPairingStore.__init__V   s%    $6 __&
    platformr   c                     t         | dz  S )Nz-pending.jsonr0   r5   r8   s     r+   _pending_pathzPairingStore._pending_path\   s    z777r7   c                     t         | dz  S )Nz-approved.jsonr:   r;   s     r+   _approved_pathzPairingStore._approved_path_   s    z888r7   c                     t         dz  S )Nz_rate_limits.jsonr:   r4   s    r+   _rate_limit_pathzPairingStore._rate_limit_pathb   s    000r7   r   c                     |j                         r&	 t        j                  |j                  d            S i S # t        j                  t
        f$ r i cY S w xY w)Nr   r   )existsjsonloads	read_textJSONDecodeErrorr%   )r5   r   s     r+   
_load_jsonzPairingStore._load_jsone   sR    ;;=zz$..'."BCC 	 (('2 	s   $9 AAr   Nc                 H    t        |t        j                  |dd             y )N   F)indentensure_ascii)r,   rC   dumps)r5   r   r   s      r+   
_save_jsonzPairingStore._save_jsonm   s    dDJJtAEJKr7   user_idc                 J    | j                  | j                  |            }||v S )z3Check if a user is approved (paired) on a platform.)rG   r>   )r5   r8   rN   approveds       r+   is_approvedzPairingStore.is_approvedr   s&    ??4#6#6x#@A(""r7   c                     g }|r|gn| j                  d      }|D ]P  }| j                  | j                  |            }|j                         D ]  \  }}|j	                  ||d|        R |S )z5List approved users, optionally filtered by platform.rP   )r8   rN   )_all_platformsrG   r>   itemsappend)r5   r8   results	platformsprP   uidinfos           r+   list_approvedzPairingStore.list_approvedw   s    "*XJ0C0CJ0O	 	HAt':':1'=>H%^^- H	TA#FFGH	H r7   	user_namec                     | j                  | j                  |            }|t        j                         d||<   | j                  | j                  |      |       y)zAAdd a user to the approved list. Must be called under self._lock.)r\   approved_atN)rG   r>   timerM   )r5   r8   rN   r\   rP   s        r+   _approve_userzPairingStore._approve_user   sN    ??4#6#6x#@A"99;
 	++H5x@r7   c                     | j                  |      }| j                  5  | j                  |      }||v r||= | j                  ||       	 ddd       y	 ddd       y# 1 sw Y   yxY w)z<Remove a user from the approved list. Returns True if found.NTF)r>   r3   rG   rM   )r5   r8   rN   r   rP   s        r+   revokezPairingStore.revoke   so    ""8,ZZ 	t,H("W%h/	 	"	 	 s   ,AA&c                 8   | j                   5  | j                  |       | j                  |      r
	 ddd       y| j                  ||      r
	 ddd       y| j	                  | j                  |            }t        |      t        k\  r
	 ddd       ydj                  d t        t              D              }||t        j                         d||<   | j                  | j                  |      |       | j                  ||       |cddd       S # 1 sw Y   yxY w)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
        N c              3   N   K   | ]  }t        j                  t                y wN)secretschoiceALPHABET).0_s     r+   	<genexpr>z-PairingStore.generate_code.<locals>.<genexpr>   s     P7>>(3Ps   #%)rN   r\   
created_at)r3   _cleanup_expired_is_locked_out_is_rate_limitedrG   r<   lenMAX_PENDING_PER_PLATFORMjoinrangeCODE_LENGTHr_   rM   _record_rate_limit)r5   r8   rN   r\   pendingcodes         r+   generate_codezPairingStore.generate_code   s    ZZ 	!!(+ ""8,	 	 $$Xw7	 	 ood&8&8&BCG7|77	 	" 77PU;=OPPD #&"iikGDM
 OOD..x8'B ##Hg6=	 	 	s   $DD3DA4DDrx   c           	      8   | j                   5  | j                  |       |j                         j                         }| j	                  |      r
	 ddd       y| j                  | j                  |            }||vr| j                  |       	 ddd       y|j                  |      }| j                  | j                  |      |       | j                  ||d   |j                  dd             |d   |j                  dd      dcddd       S # 1 sw Y   yxY w)aT  
        Approve a pairing code. Adds the user to the approved list.

        Returns {user_id, user_name} on success, None if 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)``.
        NrN   r\   rd   )rN   r\   )r3   rn   upperstripro   rG   r<   _record_failed_attemptpoprM   r`   get)r5   r8   rx   rw   entrys        r+   approve_codezPairingStore.approve_code   s    ZZ 	!!(+::<%%'D ""8,	 	 ood&8&8&BCG7"++H5	 	" KK%EOOD..x8'B xy)9599[RT;UV !+"YY{B7/	 	 	s   AD6DA/DDc                 v   g }|r|gn| j                  d      }|D ]  }| j                  |       | j                  | j                  |            }|j	                         D ]U  \  }}t        t        j                         |d   z
  dz        }|j                  |||d   |j                  dd      |d       W  |S )z?List pending pairing requests, optionally filtered by platform.rw   rm   <   rN   r\   rd   )r8   rx   rN   r\   age_minutes)	rS   rn   rG   r<   rT   intr_   rU   r   )	r5   r8   rV   rW   rX   rw   rx   rZ   age_mins	            r+   list_pendingzPairingStore.list_pending   s    "*XJ0C0CI0N	 	A!!!$ood&8&8&;<G%mmo 
dtyy{T,-??2EF ! #I!%+r!:#*  	 r7   c                 (   | j                   5  d}|r|gn| j                  d      }|D ]Q  }| j                  | j                  |            }|t	        |      z  }| j                  | j                  |      i        S 	 ddd       |S # 1 sw Y   S xY w)z2Clear all pending requests. Returns count removed.r   rw   N)r3   rS   rG   r<   rq   rM   )r5   r8   countrW   rX   rw   s         r+   clear_pendingzPairingStore.clear_pending   s    ZZ 	;E&.
D4G4G	4RI ;//$*<*<Q*?@W% 2 21 5r:;	; 	; s   A/BBc                     | j                  | j                               }| d| }|j                  |d      }t        j                         |z
  t        k  S )z2Check if a user has requested a code too recently.:r   )rG   r@   r   r_   RATE_LIMIT_SECONDS)r5   r8   rN   limitskeylast_requests         r+   rp   zPairingStore._is_rate_limited  sP    !6!6!89
!G9%zz#q)		l*.@@@r7   c                     | j                  | j                               }| d| }t        j                         ||<   | j                  | j                         |       y)z7Record the time of a pairing request for rate limiting.r   N)rG   r@   r_   rM   )r5   r8   rN   r   r   s        r+   rv   zPairingStore._record_rate_limit  sO    !6!6!89
!G9%iiks--/8r7   c                     | j                  | j                               }d| }|j                  |d      }t        j                         |k  S )zBCheck if a platform is in lockout due to failed approval attempts.	_lockout:r   )rG   r@   r   r_   )r5   r8   r   lockout_keylockout_untils        r+   ro   zPairingStore._is_locked_out  sF    !6!6!89!(,

;2yy{]**r7   c           	      `   | j                  | j                               }d| }|j                  |d      dz   }|||<   |t        k\  rGd| }t	        j                         t
        z   ||<   d||<   t        d| dt
         dt         dd	
       | j                  | j                         |       y)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)rG   r@   r   MAX_FAILED_ATTEMPTSr_   LOCKOUT_SECONDSprintrM   )r5   r8   r   fail_keyfailsr   s         r+   r}   z#PairingStore._record_failed_attempt  s    !6!6!89z*

8Q'!+ x''%hZ0K"&))+"?F; F8'z1A/AR S.//?AHLN--/8r7   c                    | j                  |      }| j                  |      }t        j                         }|j                         D cg c]  \  }}||d   z
  t        kD  r| }}}|r|D ]  }||=  | j                  ||       yyc c}}w )zRemove expired pending codes.rm   N)r<   rG   r_   rT   CODE_TTL_SECONDSrM   )r5   r8   r   rw   nowrx   rZ   expireds           r+   rn   zPairingStore._cleanup_expired+  s    !!(+//$'iik#*==?
T4d<((,<< 
 
  "DM"OOD'* 	
s   
Br   c                    g }t         j                         D ]e  }|j                  j                  d| d      s#|j                  j	                  d| dd      }|j                  d      rU|j                  |       g |S )z:List all platforms that have data files of a given suffix.-z.jsonrd   rk   )r0   iterdirnameendswithreplace
startswithrU   )r5   r   rW   r*   r8   s        r+   rS   zPairingStore._all_platforms9  sy    	$$& 	/Avv6(%0166>>AfXU*;R@**3/$$X.		/
 r7   rf   )rd   )__name__
__module____qualname____doc__r6   r   r   r<   r>   r@   dictrG   rM   boolrQ   listr[   r`   rb   r   ry   r   r   r   r   rp   rv   ro   r}   rn   rS    r7   r+   r.   r.   L   s   '8c 8d 89s 9t 91$ 1t  Lt L4 LD L
#C ## #$ #
c T Ac AC AC AQU A	s 	S 	T 	 =?))&))69)	#)V#S # # #JS D $	c 	S 	A As At A93 9 9 9+s +t +9s 9t 9 + + +S T r7   r.   )r   rC   r   rg   r   r1   r_   pathlibr   typingr   hermes_constantsr   utilsr   ri   ru   r   r   r   rr   r   r0   r   r,   r.   r   r7   r+   <module>r      s   (  	       +   .      0)< C D 4u ur7   