
    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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)expand_whatsapp_aliasesnormalize_whatsapp_identifier)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        /home/cube/projects/richard/traning coach/.omo/evidence/nutricoach-v150-combined/st_01a0560c-r63-installed-wheel-first-claim-qa/venv/lib/python3.12/site-packages/gateway/pairing.py_secure_writer.   7   s     	KKdT2##DKK(8HLBYYr31QGGDMGGIHHQXXZ  2 	x&	HHT5! 21  		 	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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%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__[   s%    $6 __&
    platformr   c                     t         | dz  S )Nz-pending.jsonr2   r7   r:   s     r-   _pending_pathzPairingStore._pending_patha   s    z777r9   c                     t         | dz  S )Nz-approved.jsonr<   r=   s     r-   _approved_pathzPairingStore._approved_pathd   s    z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                     |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'   )r7   r   s     r-   
_load_jsonzPairingStore._load_jsonj   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.   rE   dumps)r7   r   r   s      r-   
_save_jsonzPairingStore._save_jsonr   s    dDJJtAEJKr9   user_idc                 h    t        |xs d      j                         }|dk(  rt        |      xs |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   s7    '-R(..0z!0=LLr9   c                     t        |xs d      j                         }|s
t               S || j                  ||      h}|dk(  r|j	                  t        |             |j                  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|   se    '-R(..05L 7 7+ NOz!NN2;?@r9   leftrightc                 v    | j                  ||      }| j                  ||      }t        |xr	 |xr ||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   s@    ,,Xt<--h>LU]U}8TVVr9   c                 |    | j                  | j                  |            }|D ]  }| j                  |||      s y y)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   sA    ??4#6#6x#@A (##H.>H !) r9   c                     g }|r|gn| j                  d      }|D ]P  }| j                  | j                  |            }|j                         D ]  \  }}|j	                  ||d|        R |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   sr    "*XJ0C0CJ0O	At':':1'=>H%^^-	TA#FFG .  r9   	user_namec                 @   | j                  | j                  |            }| j                  ||      }|D cg c]  }| j                  |||      r| }}|D ]  }||=  |t	        j                         d||<   | j                  | j                  |      |       yc c}w )zAAdd a user to the approved list. Must be called under self._lock.)rr   approved_atN)rI   r@   rV   rc   timerO   )r7   r:   rP   rr   re   normalized_user_idrf   duplicate_idss           r-   _approve_userzPairingStore._approve_user   s    ??4#6#6x#@A!44XwG %-
$, ##H.>@RS $, 	 

 !.)* !. #99;(
#$ 	++H5x@
s   Bc           	      *   | j                  |      }| j                  5  | j                  |      }|D cg c]  }| j                  |||      r| }}|r&|D ]  }||=  | j	                  ||       	 ddd       y	 ddd       yc c}w # 1 sw Y   yxY w)z<Remove a user from the approved list. Returns True if found.NTF)r@   r5   rI   rc   rO   )r7   r:   rP   r   re   rf   matching_idss          r-   revokezPairingStore.revoke   s    ""8,ZZt,H )1(0$''2BGL !(0  
 (4$ !12 )5h/ Z     s   B	B!B	B		Bcodesaltc                 l    t        j                  || j                  d      z         j                         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   s*     ~~dT[[%99:DDFFr9   c                    | j                   5  | j                  |       | 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                  ||      }t        j                   d      }	||j#                         ||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

        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   N   K   | ]  }t        j                  t                y wN)secretschoiceALPHABET).0_s     r-   	<genexpr>z-PairingStore.generate_code.<locals>.<genexpr>   s     P=O7>>(3=Os   #%   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hexru   rO   _record_rate_limit)
r7   r:   rP   rr   rv   pendingr|   r}   	code_hashentry_ids
             r-   generate_codezPairingStore.generate_code   sD    ZZ!!(+!%!8!87!K ""8, Z $$Xw7 Z ood&8&8&BCG7|77 Z$ 77PU;=OPPD ::b>Dd3I ((+H "
-&"iik!GH OOD..x8'B ##Hg6Q ZZs   6E.E.(3E.$C E..E7c           	      6   | j                   5  | j                  |       |j                         j                         }| j	                  |      r
	 ddd       y| j                  | j                  |            }d}d}|j                         D ]i  \  }}t        |t              sd|vsd|vr 	 t        j                  |d         }| j                  ||      }	t        j                  |	|d         se|}|} n || j!                  |       	 ddd       y||= | j#                  | j                  |      |       | j%                  ||d   |j'                  dd             |d   |j'                  dd      dcddd       S # t        $ r Y w xY w# 1 sw Y   yxY w)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   rx   get)
r7   r:   r|   r   matched_keymatched_entryr   entryr}   candidate_hashs
             r-   approve_codezPairingStore.approve_code  s    ZZ!!(+::<%%'D ""8, Z ood&8&8&BCG K M#*==?%!%.&&*= ==v7D "&t!<)).%-H"*K$)M $3 "++H5O ZR $OOD..x8'B xy)A,00bAC )3*..{B?a Z: " ; ZsD   AFAF-F +F1FA!F 	F	FFFFc                    g }| j                   5  |r|gn| j                  d      }|D ]  }| j                  |       | j                  | j	                  |            }|j                         D ]  \  }}t        |t              s|j                  d      }t        |t        t        f      s?t        t        j                         |z
  dz        }	|j                  d      }
t        |
t              r|
dd nd}|j                  |||j                  dd	      |j                  d
d	      |	d        
 	 ddd       |S # 1 sw Y   |S xY w)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floatru   r   rk   )r7   r:   rl   rm   rn   r   r   rp   r   age_minhash_valcode_displays               r-   list_pendingzPairingStore.list_pendingH  s%    ZZ&.
D4G4G	4RI%%a(//$*<*<Q*?@&-mmoNHd%dD1 !%,!7J%j3,? !499;#;r"ABG#xx/H3=h3L8BQ<RZLNN$% ,#'88Ir#:%)XXk2%>'.$  '6  * + * s   D$D>>E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   r   N)r5   ri   rI   r>   r   rO   )r7   r:   countrm   rn   r   s         r-   clear_pendingzPairingStore.clear_pendingi  s    ZZE&.
D4G4G	4RI//$*<*<Q*?@W% 2 21 5r:     s   A/BBc                     | j                  | j                               }| j                  ||      D ];  }| d| }|j                  |d      }t	        j                         |z
  t
        k  s; y y)z2Check if a user has requested a code too recently.:r   TF)rI   rB   r\   r   ru   RATE_LIMIT_SECONDS)r7   r:   rP   limitsaliaskeylast_requests          r-   r   zPairingStore._is_rate_limitedv  sl    !6!6!89**8W=EJaw'C!::c1-L		l*.@@	 >
 r9   c                     | j                  | j                               }t        j                         }| j                  ||      D ]  }| d| }|||<    | j	                  | j                         |       y)z7Record the time of a pairing request for rate limiting.r   N)rI   rB   ru   r\   rO   )r7   r:   rP   r   nowr   r   s          r-   r   zPairingStore._record_rate_limit  sm    !6!6!89iik**8W=EJaw'CF3K > 	--/8r9   c                     | j                  | j                               }d| }|j                  |d      }t        j                         |k  S )zBCheck if a platform is in lockout due to failed approval attempts.	_lockout:r   )rI   rB   r   ru   )r7   r:   r   lockout_keylockout_untils        r-   r   zPairingStore._is_locked_out  sF    !6!6!89!(,

;2yy{]**r9   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)rI   rB   r   MAX_FAILED_ATTEMPTSru   LOCKOUT_SECONDSprintrO   )r7   r:   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--/8r9   c                    | j                  |      }| j                  |      }t        j                         }g }|j                         D ]~  \  }}t	        |t
              s|j                  |       (|j                  d      }t	        |t        t        f      s|j                  |       a||z
  t        kD  sn|j                  |        |r|D ]  }||=  | j                  ||       yy)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   ru   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     !!(+//$'iik%mmoNHddD)x(,/Jj3,7x(j $44x( . #H% $OOD'* r9   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.jsonrR   r   )r2   iterdirnameendswithreplace
startswithrk   )r7   r   rm   r,   r:   s        r-   ri   zPairingStore._all_platforms  st    	$$&Avv6(%0166>>AfXU*;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   rx   r{   staticmethodr   r   r   r   r   r   r   r   r   r   r   r   r   ri    r9   r-   r0   r0   Q   sb   '8c 8d 89s 9t 91$ 1t  Lt L4 LD L3   
 
s 
s3x 
W W3 Ws Wt WC # $ c T Ac AC AC AQU A$s S T & G GE Gc G G
 =?66&)6696	#6pBS B B BHS D B	c 	S 	 s t 93 9 9 9+s +t +9s 9t 9 + + +4S T r9   r0   )r   r   rE   r    r   r   r3   ru   pathlibr   typingr   gateway.whatsapp_identityr   r   hermes_constantsr   utilsr   r   r   r   r   r   r   r   r2   r   r.   r0   r   r9   r-   <module>r      s   (   	       ,   .      0)< C D 4q qr9   