defmodule SnmpKit.SnmpLib.SecurityTest do use ExUnit.Case, async: false alias SnmpKit.SnmpLib.Security @moduletag :snmpv3 doctest Security @moduletag :security @moduletag :phase5 describe "Security.create_user/2" do test "creates user with authentication only" do config = [ auth_protocol: :sha256, auth_password: "test_password_123", engine_id: "test_engine_001" ] assert {:ok, user} = Security.create_user("test_user", config) assert user.security_name == "test_user" assert user.auth_protocol == :sha256 assert user.priv_protocol == :none assert byte_size(user.auth_key) > 0 assert user.priv_key == <<>> assert user.engine_id == "test_engine_001" end test "creates user with authentication and privacy" do config = [ auth_protocol: :sha256, auth_password: "auth_password_456", priv_protocol: :aes256, priv_password: "priv_password_789", engine_id: "secure_engine_002" ] assert {:ok, user} = Security.create_user("secure_user", config) assert user.security_name == "secure_user" assert user.auth_protocol == :sha256 assert user.priv_protocol == :aes256 assert byte_size(user.auth_key) > 0 assert byte_size(user.priv_key) > 0 assert user.engine_id == "secure_engine_002" end test "rejects invalid configurations" do # Missing auth password when auth protocol specified config = [ auth_protocol: :sha256, engine_id: "test_engine" ] assert {:error, :missing_auth_password} = Security.create_user("user", config) # Privacy without authentication config = [ priv_protocol: :aes128, priv_password: "priv_pass", engine_id: "test_engine" ] assert {:error, :priv_requires_auth} = Security.create_user("user", config) # Missing engine ID config = [ auth_protocol: :sha256, auth_password: "auth_pass" ] assert {:error, :missing_engine_id} = Security.create_user("user", config) # Unsupported protocol config = [ auth_protocol: :invalid_proto, auth_password: "auth_pass", engine_id: "test_engine" ] assert {:error, :invalid_auth_protocol} = Security.create_user("user", config) end end describe "Security.get_security_level/1" do test "determines correct security levels" do # No auth, no priv user = %{auth_protocol: :none, priv_protocol: :none} assert Security.get_security_level(user) == :no_auth_no_priv # Auth only user = %{auth_protocol: :sha256, priv_protocol: :none} assert Security.get_security_level(user) == :auth_no_priv # Auth and priv user = %{auth_protocol: :sha256, priv_protocol: :aes256} assert Security.get_security_level(user) == :auth_priv end end describe "Security.authenticate_message/2" do test "authenticates message with SHA-256" do {:ok, user} = Security.create_user("auth_user", auth_protocol: :sha256, auth_password: "secure_auth_password", engine_id: "auth_engine_123" ) message = "test SNMP message data" assert {:ok, auth_params} = Security.authenticate_message(user, message) assert is_binary(auth_params) assert byte_size(auth_params) > 0 end test "returns empty auth params for no auth" do user = %{auth_protocol: :none} message = "test message" assert {:ok, <<>>} = Security.authenticate_message(user, message) end end describe "Security.verify_authentication/3" do test "verifies valid authentication" do {:ok, user} = Security.create_user("verify_user", auth_protocol: :sha256, auth_password: "verification_password", engine_id: "verify_engine_456" ) message = "message to verify" {:ok, auth_params} = Security.authenticate_message(user, message) assert :ok = Security.verify_authentication(user, message, auth_params) end test "rejects invalid authentication" do {:ok, user} = Security.create_user("verify_user", auth_protocol: :sha256, auth_password: "verification_password", engine_id: "verify_engine_456" ) message = "message to verify" fake_auth_params = :crypto.strong_rand_bytes(12) assert {:error, :authentication_mismatch} = Security.verify_authentication(user, message, fake_auth_params) end end describe "Security.encrypt_message/2" do test "encrypts message with AES-256" do {:ok, user} = Security.create_user("encrypt_user", auth_protocol: :sha256, auth_password: "auth_for_encryption", priv_protocol: :aes256, priv_password: "privacy_password_strong", engine_id: "encrypt_engine_789" ) plaintext = "confidential SNMP data that needs encryption" assert {:ok, {ciphertext, priv_params}} = Security.encrypt_message(user, plaintext) assert is_binary(ciphertext) assert is_binary(priv_params) assert ciphertext != plaintext assert byte_size(priv_params) > 0 end test "returns plaintext for no privacy" do user = %{priv_protocol: :none} plaintext = "unencrypted message" assert {:ok, {^plaintext, <<>>}} = Security.encrypt_message(user, plaintext) end end describe "Security.decrypt_message/3" do test "decrypts AES-256 encrypted message" do {:ok, user} = Security.create_user("decrypt_user", auth_protocol: :sha256, auth_password: "auth_for_decryption", priv_protocol: :aes256, priv_password: "strong_privacy_password", engine_id: "decrypt_engine_abc" ) original_plaintext = "secret data that was encrypted" # Encrypt first {:ok, {ciphertext, priv_params}} = Security.encrypt_message(user, original_plaintext) # Then decrypt assert {:ok, decrypted_plaintext} = Security.decrypt_message(user, ciphertext, priv_params) assert decrypted_plaintext == original_plaintext end test "handles decryption with wrong key" do # Create two users with different keys {:ok, user1} = Security.create_user("user1", auth_protocol: :sha256, auth_password: "password1", priv_protocol: :aes256, priv_password: "privpass1", engine_id: "engine1" ) {:ok, user2} = Security.create_user("user2", auth_protocol: :sha256, auth_password: "password2", priv_protocol: :aes256, priv_password: "privpass2", engine_id: "engine2" ) plaintext = "test encryption data" # Encrypt with user1 {:ok, {ciphertext, priv_params}} = Security.encrypt_message(user1, plaintext) # Try to decrypt with user2 (should fail due to wrong key) # The wrong key will typically result in invalid padding after decryption result = Security.decrypt_message(user2, ciphertext, priv_params) case result do {:error, _reason} -> # Most common case - padding validation fails with wrong key :ok {:ok, decrypted_data} -> # In rare cases decryption might succeed with garbage data # Ensure it's different from the original assert decrypted_data != plaintext, "Decryption with wrong key should produce different data" end end end describe "Security.generate_engine_id/1" do test "generates valid engine IDs" do identifier = "test.device.local" engine_id = Security.generate_engine_id(identifier) assert is_binary(engine_id) assert byte_size(engine_id) >= 5 assert byte_size(engine_id) <= 32 # Should be deterministic for same identifier engine_id2 = Security.generate_engine_id(identifier) # Actually includes timestamp, so different assert engine_id != engine_id2 end test "generates different engine IDs for different identifiers" do engine_id1 = Security.generate_engine_id("device1") engine_id2 = Security.generate_engine_id("device2") assert engine_id1 != engine_id2 end end describe "Security.build_security_params/3" do test "builds valid security parameters" do {:ok, user} = Security.create_user("param_user", auth_protocol: :sha256, auth_password: "param_password", engine_id: "param_engine_def" ) auth_params = :crypto.strong_rand_bytes(16) priv_params = :crypto.strong_rand_bytes(16) params = Security.build_security_params(user, auth_params, priv_params) assert params.authoritative_engine_id == user.engine_id assert params.user_name == user.security_name assert params.authentication_parameters == auth_params assert params.privacy_parameters == priv_params assert is_integer(params.authoritative_engine_boots) assert is_integer(params.authoritative_engine_time) end end describe "Security.update_engine_time/3" do test "updates engine time correctly" do {:ok, user} = Security.create_user("time_user", auth_protocol: :sha256, auth_password: "time_password", engine_id: "time_engine_ghi" ) new_boots = 5 new_time = 123_456 updated_user = Security.update_engine_time(user, new_boots, new_time) assert updated_user.engine_boots == new_boots assert updated_user.engine_time == new_time assert updated_user.security_name == user.security_name assert updated_user.auth_key == user.auth_key end end describe "Security.validate_user/3" do test "validates correct passwords" do auth_password = "correct_auth_password" priv_password = "correct_priv_password" {:ok, user} = Security.create_user("validate_user", auth_protocol: :sha256, auth_password: auth_password, priv_protocol: :aes256, priv_password: priv_password, engine_id: "validate_engine_jkl" ) assert :ok = Security.validate_user(user, auth_password, priv_password) end test "rejects incorrect passwords" do {:ok, user} = Security.create_user("validate_user", auth_protocol: :sha256, auth_password: "correct_auth", priv_protocol: :aes256, priv_password: "correct_priv", engine_id: "validate_engine_mno" ) assert {:error, :invalid_auth_password} = Security.validate_user(user, "wrong_auth", "correct_priv") assert {:error, :invalid_priv_password} = Security.validate_user(user, "correct_auth", "wrong_priv") end end describe "Security.info/0" do test "returns comprehensive security information" do info = Security.info() assert info.version == "5.1.0" assert info.phase == "5.1A - Security Foundation" assert is_list(info.supported_auth_protocols) assert is_list(info.supported_priv_protocols) assert is_list(info.rfc_compliance) assert is_list(info.security_levels) assert is_list(info.features) # Check that modern protocols are supported assert :sha256 in info.supported_auth_protocols assert :aes256 in info.supported_priv_protocols assert :auth_priv in info.security_levels end end describe "integration tests" do test "complete SNMPv3 security workflow" do # Step 1: Create secure user {:ok, user} = Security.create_user("workflow_user", auth_protocol: :sha256, auth_password: "strong_authentication_password", priv_protocol: :aes256, priv_password: "very_strong_privacy_password", engine_id: "workflow_engine_test" ) # Step 2: Verify security level assert Security.get_security_level(user) == :auth_priv # Step 3: Authenticate a message test_message = "This is a complete SNMPv3 test message with authentication and privacy" {:ok, auth_params} = Security.authenticate_message(user, test_message) # Step 4: Encrypt the message {:ok, {encrypted_message, priv_params}} = Security.encrypt_message(user, test_message) # Step 5: Build security parameters security_params = Security.build_security_params(user, auth_params, priv_params) # Step 6: Verify authentication (simulate receiving side) assert :ok = Security.verify_authentication(user, test_message, auth_params) # Step 7: Decrypt the message (simulate receiving side) {:ok, decrypted_message} = Security.decrypt_message(user, encrypted_message, priv_params) # Step 8: Verify complete round-trip assert decrypted_message == test_message assert security_params.user_name == user.security_name assert security_params.authoritative_engine_id == user.engine_id end test "supports multiple security levels in same system" do # No auth user {:ok, no_auth_user} = Security.create_user("no_auth", auth_protocol: :none, engine_id: "multi_engine_test" ) # Auth only user {:ok, auth_user} = Security.create_user("auth_only", auth_protocol: :sha256, auth_password: "auth_password", engine_id: "multi_engine_test" ) # Auth + priv user {:ok, full_user} = Security.create_user("full_security", auth_protocol: :sha256, auth_password: "auth_password", priv_protocol: :aes256, priv_password: "priv_password", engine_id: "multi_engine_test" ) # Verify different security levels assert Security.get_security_level(no_auth_user) == :no_auth_no_priv assert Security.get_security_level(auth_user) == :auth_no_priv assert Security.get_security_level(full_user) == :auth_priv # All should work with appropriate operations message = "test message for all users" # No auth user {:ok, <<>>} = Security.authenticate_message(no_auth_user, message) {:ok, {^message, <<>>}} = Security.encrypt_message(no_auth_user, message) # Auth user {:ok, auth_params} = Security.authenticate_message(auth_user, message) assert byte_size(auth_params) > 0 {:ok, {^message, <<>>}} = Security.encrypt_message(auth_user, message) # Full user {:ok, auth_params} = Security.authenticate_message(full_user, message) {:ok, {encrypted, priv_params}} = Security.encrypt_message(full_user, message) assert byte_size(auth_params) > 0 assert byte_size(priv_params) > 0 assert encrypted != message end end describe "error handling and edge cases" do test "handles empty and invalid inputs gracefully" do # Empty passwords config = [ auth_protocol: :sha256, auth_password: "", engine_id: "test_engine" ] assert {:error, _reason} = Security.create_user("test", config) # Invalid engine ID config = [ auth_protocol: :sha256, auth_password: "valid_password", engine_id: "" ] assert {:error, _reason} = Security.create_user("test", config) end test "handles protocol edge cases" do # All supported auth protocols should work auth_protocols = [:md5, :sha1, :sha256, :sha384, :sha512] for protocol <- auth_protocols do config = [ auth_protocol: protocol, auth_password: "test_password_for_#{protocol}", engine_id: "edge_case_engine" ] assert {:ok, _user} = Security.create_user("user_#{protocol}", config) end # All supported priv protocols should work priv_protocols = [:des, :aes128, :aes192, :aes256] for protocol <- priv_protocols do config = [ auth_protocol: :sha256, auth_password: "auth_password", priv_protocol: protocol, priv_password: "priv_password_for_#{protocol}", engine_id: "edge_case_engine" ] assert {:ok, _user} = Security.create_user("user_#{protocol}", config) end end end end