@@ -683,82 +683,201 @@ encode_user(DbRef, #gpg_identity{
683683 last_seen = LastSeen ,
684684 metadata = Meta
685685}) ->
686- Username = case get_username_from_gpg_fp (DbRef , Fp ) of
687- {ok , Name } -> list_to_binary (Name );
688- {error , _Reason } -> <<" unknown" >>
686+ DisplayName = case get_username_for_gpg (DbRef , Fp ) of
687+ {ok , Name } -> to_binary (Name );
688+ undefined -> <<" unknown" >>
689+ end ,
690+ % % Online check: extract the chat username from the cert (SAN then CN)
691+ % % which matches the key stored in the CONNECTION_TABLE.
692+ ChatUsername = get_chat_username_from_cert (DbRef , Fp ),
693+ Online = case ChatUsername of
694+ undefined -> false ;
695+ U -> is_online (U )
689696 end ,
690697 UserMap = #{
691698 gpg_fp => Fp ,
692- username => Username ,
699+ username => DisplayName ,
693700 status => Status ,
694701 registered_by => RegBy ,
695702 registered_at => RegAt ,
696703 last_seen => LastSeen ,
697- online => is_online ( Username )
704+ online => Online
698705 },
699706 maybe_attach_metadata (UserMap , Meta ).
700707
701- maybe_attach_metadata (UserMap , undefined ) ->
702- UserMap ;
703- maybe_attach_metadata (UserMap , Meta ) ->
704- try
705- MetaMap = jsx :decode (Meta , [return_maps ]),
706- UserMap #{metadata => MetaMap }
707- catch
708- _ :_ -> UserMap
708+ % % @private Resolve a display name for a GPG fingerprint.
709+ % % Tries the GPG key's user ID name first, then falls back to certificate CN.
710+ get_username_for_gpg (DbRef , GpgFp ) ->
711+ case get_name_from_gpg_key (DbRef , GpgFp ) of
712+ {ok , _ } = Ok -> Ok ;
713+ undefined ->
714+ case get_cn_from_cert (DbRef , GpgFp ) of
715+ undefined -> undefined ;
716+ CN -> {ok , CN }
717+ end
718+ end .
719+
720+ % % @private Extract the human name from the GPG key's user ID.
721+ get_name_from_gpg_key (DbRef , GpgFp ) ->
722+ case cryptic_ca_store :get_gpg_identity (DbRef , GpgFp ) of
723+ {ok , # gpg_identity {gpg_pub_armor = PubArmor }}
724+ when is_binary (PubArmor ), byte_size (PubArmor ) > 0 ->
725+ try
726+ case erl_gpg_api :get_key_info (PubArmor , " " ) of
727+ {ok , KeyInfo } ->
728+ UIDs = maps :get (user_ids , KeyInfo , []),
729+ extract_name_from_uids (UIDs );
730+ _ ->
731+ undefined
732+ end
733+ catch
734+ _ :_ -> undefined
735+ end ;
736+ _ ->
737+ undefined
738+ end .
739+
740+ extract_name_from_uids ([]) -> undefined ;
741+ extract_name_from_uids ([UID | Rest ]) ->
742+ case extract_name_from_uid (UID ) of
743+ {ok , _ } = Ok -> Ok ;
744+ undefined -> extract_name_from_uids (Rest )
709745 end .
710746
711- get_username_from_gpg_fp (DbRef , GpgFp ) ->
747+ extract_name_from_uid (UID ) when is_binary (UID ) ->
748+ S = unicode :characters_to_list (UID ),
749+ case string :split (S , " <" ) of
750+ [S ] ->
751+ case string :trim (S ) of
752+ [] -> undefined ;
753+ Trimmed -> {ok , Trimmed }
754+ end ;
755+ [Before , _ ] ->
756+ case string :trim (Before ) of
757+ [] -> undefined ;
758+ Name -> {ok , Name }
759+ end
760+ end ;
761+ extract_name_from_uid (_ ) -> undefined .
762+
763+ % % @private Extract the chat username from a stored certificate PEM.
764+ % % Mirrors cryptic_ws_handler: tries SAN (otherName, rfc822Name, dNSName) first,
765+ % % then falls back to CN. Returns the string that matches the CONNECTION_TABLE key.
766+ get_chat_username_from_cert (DbRef , GpgFp ) ->
712767 case cryptic_ca_store :list_certificates_by_user (DbRef , GpgFp ) of
713- {ok , []} ->
714- {error , not_found };
715- {ok , [LatestCert | _ ]} ->
716- extract_username_from_cert_pem (LatestCert # certificate .cert_pem );
717- {error , Reason } ->
718- {error , Reason }
768+ {ok , [# certificate {cert_pem = CertPem } | _ ]} ->
769+ try
770+ [{'Certificate' , CertDER , not_encrypted }] =
771+ public_key :pem_decode (CertPem ),
772+ Cert = public_key :pkix_decode_cert (CertDER , otp ),
773+ TBSCert = Cert # 'OTPCertificate' .tbsCertificate ,
774+ case extract_username_from_san (TBSCert ) of
775+ {ok , Username } -> Username ;
776+ not_found ->
777+ Subject = TBSCert # 'OTPTBSCertificate' .subject ,
778+ extract_common_name (Subject )
779+ end
780+ catch
781+ _ :_ -> undefined
782+ end ;
783+ _ ->
784+ undefined
719785 end .
720786
721- extract_username_from_cert_pem (CertPem ) ->
722- try
723- [DecodedEntry | _ ] = public_key :pem_decode (CertPem ),
724- CertDer = public_key :pem_entry_decode (DecodedEntry ),
725- Cert = public_key :pkix_decode_cert (CertDer , otp ),
726- TBSCert = Cert # 'OTPCertificate' .tbsCertificate ,
727- Subject = TBSCert # 'OTPTBSCertificate' .subject ,
728- case extract_common_name (Subject ) of
729- {ok , CN } -> {ok , CN };
730- _ -> {error , no_cn }
731- end
732- catch
733- _ :_ ->
734- {error , cert_decode_failed }
787+ % % @private Extract username from cert SAN extension.
788+ extract_username_from_san (TBSCert ) ->
789+ case TBSCert # 'OTPTBSCertificate' .extensions of
790+ asn1_NOVALUE -> not_found ;
791+ Extensions ->
792+ case lists :keyfind (?'id-ce-subjectAltName' ,
793+ # 'Extension' .extnID , Extensions ) of
794+ false -> not_found ;
795+ # 'Extension' {extnValue = GeneralNames }
796+ when is_list (GeneralNames ) ->
797+ extract_username_from_san_names (GeneralNames );
798+ _ -> not_found
799+ end
800+ end .
801+
802+ extract_username_from_san_names ([]) -> not_found ;
803+ extract_username_from_san_names ([{otherName , {{1 ,3 ,6 ,1 ,4 ,1 ,99999 ,1 }, Value }} | _ ]) ->
804+ case Value of
805+ {utf8String , U } when is_binary (U ) -> {ok , binary_to_list (U )};
806+ {utf8String , U } when is_list (U ) -> {ok , U };
807+ _ -> not_found
808+ end ;
809+ extract_username_from_san_names ([{rfc822Name , Email } | Rest ]) ->
810+ EmailStr = if is_binary (Email ) -> binary_to_list (Email );
811+ is_list (Email ) -> Email end ,
812+ case string :split (EmailStr , " @" ) of
813+ [Local , _ ] -> {ok , Local };
814+ _ -> extract_username_from_san_names (Rest )
815+ end ;
816+ extract_username_from_san_names ([{dNSName , Name } | Rest ]) ->
817+ NameStr = if is_binary (Name ) -> binary_to_list (Name );
818+ is_list (Name ) -> Name end ,
819+ case string :find (NameStr , " ." ) of
820+ nomatch -> {ok , NameStr };
821+ _ -> extract_username_from_san_names (Rest )
822+ end ;
823+ extract_username_from_san_names ([_ | Rest ]) ->
824+ extract_username_from_san_names (Rest ).
825+
826+ % % @private Extract CN from the user's certificate.
827+ get_cn_from_cert (DbRef , GpgFp ) ->
828+ case cryptic_ca_store :list_certificates_by_user (DbRef , GpgFp ) of
829+ {ok , [# certificate {cert_pem = CertPem } | _ ]} ->
830+ try
831+ [{'Certificate' , CertDER , not_encrypted }] =
832+ public_key :pem_decode (CertPem ),
833+ Cert = public_key :pkix_decode_cert (CertDER , otp ),
834+ TBSCert = Cert # 'OTPCertificate' .tbsCertificate ,
835+ Subject = TBSCert # 'OTPTBSCertificate' .subject ,
836+ extract_common_name (Subject )
837+ catch
838+ _ :_ -> undefined
839+ end ;
840+ _ ->
841+ undefined
735842 end .
736843
737- extract_common_name ({ rdnSequence , RDNs } ) ->
738- extract_cn_from_rdns ( RDNs ).
844+ to_binary ( V ) when is_binary ( V ) -> V ;
845+ to_binary ( V ) when is_list ( V ) -> list_to_binary ( V ).
739846
740- extract_cn_from_rdns ([]) ->
741- {error , no_cn_found };
742- extract_cn_from_rdns ([RDN | Rest ]) ->
743- case extract_cn_from_rdn (RDN ) of
744- {ok , CN } -> {ok , CN };
745- not_found -> extract_cn_from_rdns (Rest )
847+ maybe_attach_metadata (UserMap , undefined ) ->
848+ UserMap ;
849+ maybe_attach_metadata (UserMap , Meta ) ->
850+ try
851+ MetaMap = jsx :decode (Meta , [return_maps ]),
852+ UserMap #{metadata => MetaMap }
853+ catch
854+ _ :_ -> UserMap
746855 end .
747856
748- extract_cn_from_rdn ([]) ->
749- not_found ;
750- extract_cn_from_rdn ([# 'AttributeTypeAndValue' {
751- type = {2 , 5 , 4 , 3 },
752- value = {_Type , Value }
753- } | _ ]) ->
754- {ok , Value };
755- extract_cn_from_rdn ([_ | Rest ]) ->
756- extract_cn_from_rdn (Rest ).
857+ extract_common_name ({rdnSequence , RDNSeq }) ->
858+ lists :foldl (fun
859+ (_ , Found ) when is_list (Found ) -> Found ;
860+ (RDNSet , undefined ) ->
861+ lists :foldl (fun
862+ (_ , Found ) when is_list (Found ) -> Found ;
863+ (# 'AttributeTypeAndValue' {
864+ type = ?'id-at-commonName' ,
865+ value = {utf8String , CN }}, _ ) ->
866+ binary_to_list (CN );
867+ (# 'AttributeTypeAndValue' {
868+ type = ?'id-at-commonName' ,
869+ value = CN }, _ ) when is_list (CN ) ->
870+ CN ;
871+ (_ , Acc ) -> Acc
872+ end , undefined , RDNSet )
873+ end , undefined , RDNSeq );
874+ extract_common_name (_ ) ->
875+ undefined .
757876
758877find_user_connection (User ) when is_binary (User ) ->
759878 find_user_connection (binary_to_list (User ));
760879find_user_connection (User ) when is_list (User ) ->
761- case ets :lookup (user_connections , User ) of
880+ case ets :lookup (? CONNECTION_TABLE , User ) of
762881 [{User , Pid }] when is_pid (Pid ) ->
763882 case is_process_alive (Pid ) of
764883 true -> {ok , Pid };
0 commit comments