
    *j4                       d Z ddlm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
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 n# e$ r eZdxZxZZY nw xY wedz  Zedz  ZdZdZ G d de          Z dIdZ!dJdZ"dKdZ#dLd Z$dMd"Z%dNd$Z&dMd%Z'dOd'Z(dPd)Z)dQd.Z*dRd0Z+dSd2Z,dTd4Z-dUd5Z.dUd6Z/e G d7 d8                      Z0dVd9Z1dWd>Z2dXd@Z3dYdCZ4dZdDZ5dZdEZ6d[dGZ7d\dHZ8dS )]zPasskey/WebAuthn helpers for Hermes WebUI.

Default-off: passkeys are only advertised after an authenticated user registers
one from Settings. Password auth remains the bootstrap/recovery mechanism.
    )annotationsN)	dataclass)Path)Any)	STATE_DIR)InvalidSignature)hashesserialization)eczpasskeys.jsonz.passkey_challenges.jsoni,  Hermes WebUIc                      e Zd ZdZdS )PasskeyErrorz.Raised for user-correctable WebAuthn failures.N)__name__
__module____qualname____doc__     //Users/bertmahoney/hermes-webui/api/passkeys.pyr   r   $   s        8888r   r   databytesreturnstrc                v    t          j        |                               d                              d          S )N   =ascii)base64urlsafe_b64encoderstripdecoder   s    r   _b64ur"   (   s/    #D))0066==gFFFr   valuestr | bytesc                   t          | t                    r|                     d          } t          |                                           } | dt          |            dz  z  z  } t          j        |                     d                    S )Nr   =   )	
isinstancer   r    r   striplenr   urlsafe_b64decodeencode)r#   s    r   _b64u_decoder-   ,   st    % &W%%JJE	SSZZK!O$$E#ELL$9$9:::r   pathr   defaultr   c                    	 |                                  r(t          j        |                     d                    S n# t          $ r |cY S w xY w|S )Nutf-8encoding)existsjsonloads	read_text	Exception)r.   r/   s     r   
_json_loadr9   4   se    ;;== 	@:dnngn>>???	@   Ns   ;? AApayloadNonec                   | j                             dd           t          j        | j         d          \  }}	 t	          j        |dd          5 }t          j        ||dd	           d d d            n# 1 swxY w Y   t	          j        |d
           t	          j	        ||            d S # t          $ r( 	 t	          j        |           n# t          $ r Y nw xY w w xY w)NT)parentsexist_okz.tmp)dirsuffixwr1   r2      )indent	sort_keysi  )parentmkdirtempfilemkstemposfdopenr5   dumpchmodreplacer8   unlinkOSError)r.   r:   fdtmpfs        r   _atomic_write_jsonrS   =   s;   KdT2224;v>>>GB
Yr3111 	<QIgqd;;;;	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	< 	<
e

3   	IcNNNN 	 	 	D	sS   B/ A8,B/ 8A<<B/ ?A< -B/ /
C!:CC!
CC!CC!list[dict[str, Any]]c                 r    t          t          g           } t          | t                    sg S d | D             S )Nc                    g | ]A}t          |t                    t          |                    d           t                    ?|BS id)r(   dictgetr   .0cs     r   
<listcomp>z%_load_credentials.<locals>.<listcomp>Q   s@    TTT!z!T22Tz!%%++s7S7STATTTr   )r9   _CREDENTIALS_FILEr(   listr!   s    r   _load_credentialsra   M   s=    ',,DdD!! 	TTtTTTTr   credsc                0    t          t          |            d S N)rS   r_   )rb   s    r   _save_credentialsre   T   s    (%00000r   c                 &   g } t                      D ]}|                     |                    d          |                    d          pd|                    d          |                    d          |                    dd          d           | S )	zAReturn public credential metadata only; never expose public keys.rX   labelPasskey
created_atlast_used_at
sign_countr   )rX   rg   ri   rj   rk   )ra   appendrZ   )outr]   s     r   registered_credentialsrn   X   s    
C    

%%++UU7^^0y%%--EE.11%%a00
 
 	 	 	 	 Jr   boolc                 8    t          t                                S rd   )ro   ra   r   r   r   passkeys_availablerq   f   s    !##$$$r   dict[str, dict[str, Any]]c                     t          t          i           } t          | t                    si S t	          j                    fd|                                 D             }|| k    rt          t          |           |S )Nc           	         i | ]a\  }}t          |t                    rGt          |t                    r2t          |                    d d                    z
  t
          k     ^||bS )tsr   )r(   r   rY   floatrZ   _CHALLENGE_TTL)r\   kvnows      r   
<dictcomp>z$_load_challenges.<locals>.<dictcomp>o   ss       Aa",Q"5"5:=aeeDRSnn@U@U:UXf:f:f 	
1:f:f:fr   )r9   _CHALLENGES_FILEr(   rY   timeitemsrS   )rawcleanrz   s     @r   _load_challengesr   j   s    
%r
*
*Cc4   	
)++C     E ||+U333Lr   	challengekindrp_idoriginc                    t                      }|||t          j                    d|| <   t          t          |           d S )N)r   r   r   ru   )r   r}   rS   r|   )r   r   r   r   r   s        r   _store_challenger   x   s?    D#evTY[[YYDO'.....r   dict[str, Any]c                    t                      }|                    | d           }t          t          |           |r|                    d          |k    rt          d          |S )Nr   z%Passkey challenge expired. Try again.)r   poprS   r|   rZ   r   )r   r   r   entrys       r   _consume_challenger   ~   sd    DHHY%%E'... DEIIf%%--BCCCLr   hostc                   | pd                                                     dd          d         } |                     d          r!d| v r| d|                     d                   S d| v r|                     dd          d         n| S )N	localhost,   r   []:)r)   split
startswithindexrsplit)r   s    r   _host_without_portr      s    K&&((..sA66q9Ds 'tAdjjoo%&&%(D[[4;;sAq!!d:r   tuple[str, str]c                   t          | j                            dd                    }| j                            dd                              dd          d                                                                         }|dvr)	 dd	lm}  ||           rd
nd}n# t          $ r d}Y nw xY w|| d| j                            d|           fS )NHostr   zX-Forwarded-Proto r   r   r   >   httphttps)_is_secure_contextr   r   z://)	r   headersrZ   r   r)   lowerapi.authr   AttributeError)handlerr   protor   s       r   
rp_contextr      s    go11&+FFGGDO 3R88>>sAFFqIOOQQWWYYE%%%	33333311'::FGGEE 	 	 	EEE	EAAgo11&$??AAAAs   B B,+B,c           
        t          |           \  }}t          t          j        d                    }t	          |d||           |t
          |dt          t          j        |                                          	                                d d                   dddddd	gd
d
dddd t                      D             dS )N    register)namerX      r   )rX   r   displayName
public-key)typealg	preferred)residentKeyuserVerification`  nonec                $    g | ]}d |d         dS r   rX   )r   rX   r   r[   s     r   r^   z(registration_options.<locals>.<listcomp>   s$    gggAdGDDgggr   )r   rpuserpubKeyCredParamsauthenticatorSelectiontimeoutattestationexcludeCredentials)r   r"   secretstoken_bytesr   _RP_NAMEhashlibsha256r,   digestrn   )r   r   _originr   s       r   registration_optionsr      s    ((NE7g)"--..IY
E7;;;u--W^ELLNN;;BBDDSbSIJJTb  tB  C  C&22>>?2=S^"_"_ggNdNfNfggg	 	 	r   c                    t                      }|st          d          t          |           \  }}t          t	          j        d                    }t          |d||           ||d |D             dddS )NzNo passkeys are registered.r   loginc                $    g | ]}d |d         dS r   r   r[   s     r   r^   z*authentication_options.<locals>.<listcomp>   s$    RRRql!D'BBRRRr   r   r   )r   rpIdallowCredentialsr   r   )rn   r   r   r"   r   r   r   )r   rb   r   r   r   s        r   authentication_optionsr      s    "$$E :8999w''ME6g)"--..IY777RRERRR'  r   c                  @    e Zd ZU ded<   dZded<   ddZdd
ZddZdS )_Cborr   r   r   intposnr   c                    | j         |z   t          | j                  k    rt          d          | j        | j         | j         |z            }| xj         |z  c_         |S )NzMalformed CBOR data)r   r*   r   r   )selfr   rm   s      r   readz
_Cbor.read   sW    8a<#di..((4555iA-.A
r   r   c                                          d          d         }|dz	  |dz  }}                     |          }|dk    r|S |dk    rd|z
  S |dk    r                      |          S |dk    r(                      |                              d          S |d	k    r fd
t          |          D             S |dk    r fdt          |          D             S |dk    r|dk    rdS |dk    rdS |dk    rd S t	          d          )Nr   r         rB      r1   r'   c                8    g | ]}                                 S r   itemr\   _r   s     r   r^   z_Cbor.item.<locals>.<listcomp>   s!    444ADIIKK444r   c                ^    i | ])}                                                                  *S r   r   r   s     r   r{   z_Cbor.item.<locals>.<dictcomp>   s+    AAADIIKKAAAr         F   T   zUnsupported CBOR data)r   _valr    ranger   )r   initialmajoraddlvals   `    r   r   z
_Cbor.item   s*   ))A,,q/lGdNtiiooA::JA::8OA::99S>>!A::99S>>((111A::4444s4444A::AAAAeCjjAAAAA::byyubyytbyyt2333r   r   c                   |dk     r|S |dk    r|                      d          d         S |dk    r.t                              |                      d          d          S |dk    r.t                              |                      d          d          S |d	k    r.t                              |                      d
          d          S t          d          )N   r   r      rB   big   r'         z(Indefinite CBOR values are not supported)r   r   
from_bytesr   )r   r   s     r   r   z
_Cbor._val   s    "99K2::99Q<<?"2::>>$))A,,6662::>>$))A,,6662::>>$))A,,666EFFFr   N)r   r   r   r   )r   r   )r   r   r   r   )r   r   r   __annotations__r   r   r   r   r   r   r   r   r      sn         KKKCLLLL   4 4 4 42G G G G G Gr   r   c                    t          |           }|                                }|j        t          |           k    rt	          d          |S )NzTrailing CBOR data)r   r   r   r*   r   )r   parserr#   s      r   _cbor_loadsr      sA    4[[FKKMMEzSYY/000Lr   encodedexpected_typechallenge_kind,tuple[dict[str, Any], dict[str, Any], bytes]c                   t          |           }	 t          j        |                    d                    }n"# t          $ r}t          d          |d }~ww xY w|                    d          |k    rt          d          |                    d          }t          |t                    st          d          t          ||          }|                    d          |                    d          k    rt          d          |||fS )	Nr1   zMalformed client datar   z Unexpected passkey response typer   zMissing passkey challenger   zPasskey origin mismatch)
r-   r5   r6   r    r8   r   rZ   r(   r   r   )r   r   r   r   r   excr   r   s           r   _client_datar      s   
w

C=z#**W--.. = = =233<=xx=((=>>>%%Ii%% 86777y.99ExxUYYx00004555s   '9 
AAA	auth_datac                   t          |           dk     rt          d          | d d         }t          j        |                    d                                                    }t          j        ||          st          d          | d         }|dz  st          d          t          	                    | dd         d	          }||| dd          d
S )N%   zMalformed authenticator datar   idnazPasskey RP ID mismatchr   z&Passkey user presence was not verified!   r   )flagsrk   rest)
r*   r   r   r   r,   r   hmaccompare_digestr   r   )r   r   rp_hashexpectedr  rk   s         r   _parse_auth_datar	    s    
9~~9:::nG~ell62233::<<Hw11 53444bMEDL ECDDD	"R% 0%88J*inMMMr   cosedict[Any, Any]c                ~   t           t          t          d          |                     d          }|                     d          }|                     d          }|                     d          }|                     d          }|dk    s6|dk    s0|dk    s*t	          |t
                    rt	          |t
                    st          d	          t          j        t                              |d
          t                              |d
          t          j	                              }|
                                S )N1Passkey support requires the cryptography packager   r   r   r   rB   z!Only ES256 passkeys are supportedr   )r   r
   r   rZ   r(   r   EllipticCurvePublicNumbersr   r   	SECP256R1
public_key)r
  r   ktycrvxynumberss          r   _public_key_from_coser    s    	z]*NOOO
((1++C
((1++C
((2,,CAA
byyC1HHq
1e0D0DJWXZ_L`L`>???+CNN1e,D,DcnnUVX]F^F^`b`l`n`nooGr   c           	        |                      d          pi }t          |                     dd          dd          \  }}}t          t          |                     dd                              }t	          |t
                    r(t	          |                     d          t                    st          d          t          |d         |d	                   }|d
         dz  st          d          |d         }t          |          dk     rt          d          t                              |dd         d          }	|dd|	z            }
|d|	z   d          }t          |          }t          |          }|                    t          j        j        t          j        j                                      d          }t)          |
          t+          |                      d          pd                                          d d         pd}fdt/                      D             }|                    |||d         t3          j                    d d           t5          |           d|ddS )NresponseclientDataJSONr   zwebauthn.creater   attestationObjectauthDatazMalformed attestation objectr   r  @   zPasskey credential data missingr     zMalformed credential datar   r   r   rg   rh   P   c                F    g | ]}|                     d           k    |S rW   rZ   )r\   r]   cred_ids     r   r^   z'finish_registration.<locals>.<listcomp>6  s-    FFF1quuT{{g/E/EQ/E/E/Er   rk   )rX   rg   public_key_pemrk   ri   rj   T)rX   rg   )ok
credential)rZ   r   r   r-   r(   rY   r   r   r	  r*   r   r   r  public_bytesr
   EncodingPEMPublicFormatSubjectPublicKeyInfor    r"   r   r)   ra   rl   r}   re   )r:   r   r  _clientr   _client_rawatt_objparsedr  cred_lencredential_id
cose_bytescose_keyr  pemrg   rb   r#  s                    @r   finish_registrationr5  "  sV   {{:&&,"H".x||<Lb/Q/QSdfp"q"qGUK,x||4G'L'LMMNNGgt$$ ;Jw{{:7N7NPU,V,V ;9:::gj15>BBF7Od" ><===&>D
4yy2~~6777~~d2b5k511HBM)*Mb8mnn%J:&&H&x00J

!
!-"8"<m>X>m
n
n
u
uv}
~
~CM""GG$$1	2288::3B3?L9EFFFF)++FFFE	LL\*ikk     eWu&E&EFFFr   c                x   t           t          t          d          |                     d          pi }|                     d          p|                     d          t	          t
                    st          d          t                      }t          fdt          |          D             d          }|dk     rt          d	          t          |                    d
d          dd          \  }}}t          |                    dd                    }t          ||d                   }	t          |                    dd                    }
t          j        t          ||                             dd                                        d                    }|t          j        |                                          z   }	 |                    |
|t%          j        t          j                                         n"# t*          $ r}t          d          |d }~ww xY wt-          ||                             d          pd          }|	d         r|r|	d         |k    rt          d          |	d         p|||         d<   t/          j                    ||         d<   t1          |           ddS )Nr  r  rX   rawIdzMissing passkey credential idc              3  T   K   | ]"\  }}|                     d           k    |V  #dS )rX   Nr"  )r\   ir]   r#  s      r   	<genexpr>zfinish_login.<locals>.<genexpr>K  s;      IIda!%%++2H2H2H2H2H2HIIr   r   r   zUnknown passkeyr  r   zwebauthn.getr   authenticatorDatar   	signaturer$  r   z%Passkey signature verification failedrk   z$Passkey sign counter did not advancerj   T)r%  r1  )r
   r	   r   rZ   r(   r   ra   next	enumerater   r-   r	  load_pem_public_keyr,   r   r   r   verifyr   ECDSASHA256r   r   r}   re   )r:   r   r  rb   idxr,  r   
client_rawr   r/  r<  r  signedr   	old_countr#  s                  @r   finish_loginrG  C  s   NOOO{{:&&,"Hkk$77;;w#7#7Ggs## <:;;;E
IIIIi..III2
N
NC
Qww,---!-hll;KR.P.PR`bi!j!jGUJX\\*=rBBCCIiw88FX\\+r::;;I23uSz~~FVXZ7[7[3\3\3c3cdk3l3lmmJ
33::<<<FM)VRXfmoo-F-FGGGG M M MBCCLME#JNN<005A66Il C	 Cf\.Bi.O.OABBB%l3@yE#J|!%E#J~e111s   :H 
H'H""H'r1  c                     t                      } fd|D             }t          |          t          |          k    rt          d          t          |           dt	                      dS )Nc                F    g | ]}|                     d           k    |S rW   r"  )r\   r]   r1  s     r   r^   z%delete_credential.<locals>.<listcomp>c  s-    ===!d} < <A < < <r   zPasskey not foundT)r%  credentials)ra   r*   r   re   rn   )r1  rb   kepts   `  r   delete_credentialrL  a  sp    E====u===D
4yyCJJ.///d'='?'?@@@r   c                 Z    t                                           rt          g            dS dS )z?Remove all registered passkeys when the user disables all auth.N)r_   r4   re   r   r   r   clear_credentialsrN  j  s4    !! " r   )r   r   r   r   )r#   r$   r   r   )r.   r   r/   r   r   r   )r.   r   r:   r   r   r;   )r   rT   )rb   rT   r   r;   )r   ro   )r   rr   )
r   r   r   r   r   r   r   r   r   r;   )r   r   r   r   r   r   )r   r   r   r   )r   r   )r   r   )r   r   r   r   )r   r   r   r   r   r   r   r   )r   r   r   r   r   r   )r
  r  )r:   r   r   r   )r1  r   r   r   )r   r;   )9r   
__future__r   r   r   r  r5   rI   r   rG   r}   dataclassesr   pathlibr   typingr   
api.configr   cryptography.exceptionsr   cryptography.hazmat.primitivesr	   r
   )cryptography.hazmat.primitives.asymmetricr   r8   r_   r|   rw   r   
ValueErrorr   r"   r-   r9   rS   ra   re   rn   rq   r   r   r   r   r   r   r   r   r   r   r	  r  r5  rG  rL  rN  r   r   r   <module>rX     s   
 # " " " " "     				    ! ! ! ! ! !                        '888888DDDDDDDD<<<<<<< ' ' ' "&&F&]RRR' / 99 9 9 9 9 9: 9 9 9G G G G; ; ; ;       U U U U1 1 1 1   % % % %   / / / /   ; ; ; ;	B 	B 	B 	B         /G /G /G /G /G /G /G /Gd      "N N N N       G G G GB2 2 2 2<A A A A     s   A A'&A'