
    i,                         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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)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,   2   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 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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__V   s0    $666 _&&


    platformr   c                     t           | dz  S )Nz-pending.jsonr0   r5   r8   s     r+   _pending_pathzPairingStore._pending_path\   s    77777r7   c                     t           | dz  S )Nz-approved.jsonr:   r;   s     r+   _approved_pathzPairingStore._approved_path_   s    88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                     |                                 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%   )r5   r   s     r+   
_load_jsonzPairingStore._load_jsone   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,   rC   dumps)r5   r   r   s      r+   
_save_jsonzPairingStore._save_jsonm   s)    dDJtAEJJJKKKKKr7   user_idc                 Z    |                      |                     |                    }||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A(""r7   c                     g }|r|gn|                      d          }|D ]^}|                     |                     |                    }|                                D ]\  }}|                    ||d|            _|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    "*OXJJ0C0CJ0O0O	 	H 	HAt':':1'='=>>H%^^-- H H	TA#FFFGGGGHr7    	user_namec                     |                      |                     |                    }|t          j                    d||<   |                     |                     |          |           dS )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   sh    ??4#6#6x#@#@AA"9;;
 
 	++H55x@@@@@r7   c                     |                      |          }| j        5  |                     |          }||v r'||= |                     ||           	 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.NTF)r>   r3   rG   rM   )r5   r8   rN   r   rP   s        r+   revokezPairingStore.revoke   s    ""8,,Z 	 	t,,H(""W%h///	 	 	 	 	 	 	 	"	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 us   4A++A/2A/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||<   |                     |                     |          |           |                     ||           |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
        Nr\   c              3   H   K   | ]}t          j        t                    V  d S N)secretschoiceALPHABET).0_s     r+   	<genexpr>z-PairingStore.generate_code.<locals>.<genexpr>   s,      PP7>(33PPPPPPr7   )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    Z 	 	!!(+++ ""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 #&"ikk GDM
 OOD..x88'BBB ##Hg666=	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s%   ,EE%AE3BEEErx   c           	      t   | j         5  |                     |           |                                                                }|                     |                     |                    }||vr#|                     |           	 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 )z
        Approve a pairing code. Adds the user to the approved list.

        Returns {user_id, user_name} on success, None if code is invalid/expired.
        NrN   r]   r\   )rN   r]   )r3   rn   upperstriprG   r<   _record_failed_attemptpoprM   ra   get)r5   r8   rx   rw   entrys        r+   approve_codezPairingStore.approve_code   s    Z 	 	!!(+++::<<%%''Dood&8&8&B&BCCG7""++H555	 	 	 	 	 	 	 	 KK%%EOOD..x88'BBB xy)9599[RT;U;UVVV !+"YY{B77 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   A>D-BD--D14D1c                    g }|r|gn|                      d          }|D ]}|                     |           |                     |                     |                    }|                                D ]f\  }}t          t          j                    |d         z
  dz            }|                    |||d         |                    dd          |d           g|S )z?List pending pairing requests, optionally filtered by platform.rw   rm   <   rN   r]   r\   )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    "*NXJJ0C0CI0N0N	 	 	A!!!$$$ood&8&8&;&;<<G%mmoo  
dty{{T,-??2EFF ! #I!%+r!:!:#*        r7   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   rw   N)r3   rS   rG   r<   rq   rM   )r5   r8   countrW   rX   rw   s         r+   clear_pendingzPairingStore.clear_pending   s    Z 	; 	;E&.R

D4G4G	4R4RI ; ;//$*<*<Q*?*?@@W% 2 21 5 5r::::;	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; s   BBB!Bc                     |                      |                                           }| d| }|                    |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   s\    !6!6!8!899%%G%%zz#q))	l*.@@@r7   c                     |                      |                                           }| d| }t          j                    ||<   |                     |                                 |           dS )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  sg    !6!6!8!899%%G%%ikks--//88888r7   c                     |                      |                                           }d| }|                    |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	  sP    !6!6!8!899,(,,

;22y{{]**r7   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)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!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8r7   c                    |                      |          }|                     |          }t          j                    fd|                                D             }|r |D ]}||= |                     ||           dS dS )zRemove expired pending codes.c                 B    g | ]\  }}|d          z
  t           k    |S )rm   )CODE_TTL_SECONDS)rj   rx   rZ   nows      r+   
<listcomp>z1PairingStore._cleanup_expired.<locals>.<listcomp>%  s=     
 
 
T4d<((,<<< <<<r7   N)r<   rG   r`   rT   rM   )r5   r8   r   rw   expiredrx   r   s         @r+   rn   zPairingStore._cleanup_expired   s    !!(++//$''ikk
 
 
 
#*==??
 
 
  	+ " "DMMOOD'*****	+ 	+r7   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.jsonr\   rk   )r0   iterdirnameendswithreplace
startswithrU   )r5   r   rW   r*   r8   s        r+   rS   zPairingStore._all_platforms.  s    	$$&& 	/ 	/Av0600011 /6>>*;f*;*;*;R@@**3// /$$X...r7   rf   )r\   )__name__
__module____qualname____doc__r6   r   r   r<   r>   r@   dictrG   rM   boolrQ   listr[   ra   rc   r   ry   r   r   r   r   rp   rv   ro   r}   rn   rS    r7   r+   r.   r.   L   s5        ' ' '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
#C ## #$ # # # #
 c T    A Ac AC AC AQU A A A A	s 	S 	T 	 	 	 	 =?) ))&))69)	#) ) ) )VS      4 S D    $	 	c 	S 	 	 	 	A As At A A A A93 9 9 9 9 9 9+s +t + + + +9s 9t 9 9 9 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      s6   (  				                    + + + + + +             .      n0)<< C D    4j j j j j j j j j jr7   