@@ -557,6 +557,23 @@ SecurityResult LSLSecurity::load_credentials() {
557557 return SecurityResult::CONFIG_NOT_FOUND ;
558558}
559559
560+ void LSLSecurity::reset () {
561+ // Return to the initialized-but-unconfigured state without touching the
562+ // libsodium initialization. Used for test isolation in this process-global
563+ // singleton so a credential-loading test does not enable security for later
564+ // tests.
565+ enabled_ = false ;
566+ credentials_loaded_ = false ;
567+ key_locked_ = false ;
568+ has_encrypted_key_ = false ;
569+ secure_zero (secret_key_.data (), secret_key_.size ());
570+ secure_zero (x25519_secret_key_.data (), x25519_secret_key_.size ());
571+ public_key_.fill (0 );
572+ secret_key_.fill (0 );
573+ x25519_public_key_.fill (0 );
574+ x25519_secret_key_.fill (0 );
575+ }
576+
560577SecurityResult LSLSecurity::convert_ed25519_to_x25519 () {
561578 // Convert Ed25519 public key to X25519
562579 if (crypto_sign_ed25519_pk_to_curve25519 (
@@ -765,47 +782,78 @@ uint32_t LSLSecurity::get_session_key_lifetime() const {
765782 return session_key_lifetime_;
766783}
767784
768- SecurityResult LSLSecurity::derive_session_key (
769- const std::array<uint8_t , PUBLIC_KEY_SIZE >& peer_public_key,
770- std::array<uint8_t , SESSION_KEY_SIZE >& session_key,
771- bool is_initiator) {
785+ SecurityResult LSLSecurity::generate_ephemeral_keypair (
786+ std::array<uint8_t , 32 >& eph_public,
787+ std::array<uint8_t , 32 >& eph_secret) {
788+
789+ if (!initialized_) {
790+ return SecurityResult::NOT_INITIALIZED ;
791+ }
792+
793+ // Fresh X25519 keypair for this connection only.
794+ if (crypto_box_keypair (eph_public.data (), eph_secret.data ()) != 0 ) {
795+ return SecurityResult::KEY_GENERATION_FAILED ;
796+ }
797+
798+ return SecurityResult::SUCCESS ;
799+ }
800+
801+ SecurityResult LSLSecurity::derive_session_key_ephemeral (
802+ const std::array<uint8_t , 32 >& own_eph_secret,
803+ const std::array<uint8_t , 32 >& peer_eph_public,
804+ std::array<uint8_t , SESSION_KEY_SIZE >& session_key) {
772805
773806 if (!initialized_ || !credentials_loaded_) {
774807 return SecurityResult::NOT_INITIALIZED ;
775808 }
776809
777- // Convert peer's Ed25519 public key to X25519
778- std::array<uint8_t , 32 > peer_x25519;
779- if (crypto_sign_ed25519_pk_to_curve25519 (peer_x25519.data (), peer_public_key.data ()) != 0 ) {
810+ // Ephemeral X25519 agreement: shared = X25519(own_eph_secret, peer_eph_public).
811+ // Because both ephemeral keys are random and fresh per connection, this shared
812+ // secret is unique per session and is forgotten once the secrets are zeroed.
813+ std::array<uint8_t , crypto_scalarmult_BYTES> shared_secret;
814+ if (crypto_scalarmult (shared_secret.data (), own_eph_secret.data (),
815+ peer_eph_public.data ()) != 0 ) {
816+ return SecurityResult::INVALID_KEY ;
817+ }
818+ // Reject a degenerate all-zero shared secret (peer sent a low-order point).
819+ if (sodium_is_zero (shared_secret.data (), shared_secret.size ())) {
820+ secure_zero (shared_secret.data (), shared_secret.size ());
780821 return SecurityResult::INVALID_KEY ;
781822 }
782823
783- // X25519 key agreement
784- std::array<uint8_t , crypto_scalarmult_BYTES> shared_secret;
785- if (crypto_scalarmult (shared_secret.data (), x25519_secret_key_.data (), peer_x25519.data ()) != 0 ) {
824+ // Recompute our ephemeral public from the secret so the transcript can bind
825+ // both ephemeral public keys without the caller having to pass it back in.
826+ std::array<uint8_t , 32 > own_eph_public;
827+ if (crypto_scalarmult_base (own_eph_public.data (), own_eph_secret.data ()) != 0 ) {
828+ secure_zero (shared_secret.data (), shared_secret.size ());
786829 return SecurityResult::INVALID_KEY ;
787830 }
788831
789- // Derive session key from shared secret and both public keys
790832 crypto_generichash_state state;
791833 crypto_generichash_init (&state, nullptr , 0 , SESSION_KEY_SIZE );
792834 crypto_generichash_update (&state, shared_secret.data (), shared_secret.size ());
793- crypto_generichash_update (&state, (const uint8_t *)HKDF_CONTEXT , sizeof (HKDF_CONTEXT ) - 1 );
835+ crypto_generichash_update (&state, (const uint8_t *)EPH_CONTEXT , sizeof (EPH_CONTEXT ) - 1 );
794836
795- // Order public keys consistently (smaller first) so both parties derive same key
796- if (memcmp (public_key_.data (), peer_public_key.data (), PUBLIC_KEY_SIZE ) < 0 ) {
797- crypto_generichash_update (&state, public_key_.data (), PUBLIC_KEY_SIZE );
798- crypto_generichash_update (&state, peer_public_key.data (), PUBLIC_KEY_SIZE );
837+ // Order the ephemeral public keys consistently (smaller first) so initiator
838+ // and responder derive the same key without exchanging role information.
839+ if (memcmp (own_eph_public.data (), peer_eph_public.data (), 32 ) < 0 ) {
840+ crypto_generichash_update (&state, own_eph_public.data (), 32 );
841+ crypto_generichash_update (&state, peer_eph_public.data (), 32 );
799842 } else {
800- crypto_generichash_update (&state, peer_public_key .data (), PUBLIC_KEY_SIZE );
801- crypto_generichash_update (&state, public_key_ .data (), PUBLIC_KEY_SIZE );
843+ crypto_generichash_update (&state, peer_eph_public .data (), 32 );
844+ crypto_generichash_update (&state, own_eph_public .data (), 32 );
802845 }
803846
847+ // Bind the session key to the shared long-term identity (group membership).
848+ crypto_generichash_update (&state, public_key_.data (), PUBLIC_KEY_SIZE );
849+
804850 crypto_generichash_final (&state, session_key.data (), SESSION_KEY_SIZE );
805851
806- // Zero shared secret
852+ // Zero all working material: shared_secret and the hash state hold (or are
853+ // derived from) the shared secret; own_eph_public is public but cleared too.
807854 secure_zero (shared_secret.data (), shared_secret.size ());
808- secure_zero (peer_x25519.data (), peer_x25519.size ());
855+ secure_zero (own_eph_public.data (), own_eph_public.size ());
856+ sodium_memzero (&state, sizeof (state));
809857
810858 return SecurityResult::SUCCESS ;
811859}
0 commit comments