diff --git a/lib/safety/encryption.ex b/lib/safety/encryption.ex new file mode 100644 index 0000000..d324873 --- /dev/null +++ b/lib/safety/encryption.ex @@ -0,0 +1,93 @@ +defmodule ActivityPub.Safety.Encryption do + @moduledoc """ + Provides encryption and decryption functionality using RSA keys managed by ActivityPub.Safety.Keys. + + NOTE: not used at the moment, simply intended as a proof-of-concept + """ + import Untangle + + alias ActivityPub.Safety.Keys + alias ActivityPub.Actor + + @doc """ + Encrypts data for a given actor using their public key. + + ## Parameters + - data: The data to encrypt (binary or string) + - actor: The Actor struct or AP ID of the recipient + + ## Returns + - {:ok, encrypted_data} on success + - {:error, reason} on failure + """ + def encrypt(data, ap_id) when is_binary(ap_id) do + with {:ok, public_key} <- Keys.get_public_key_for_ap_id(ap_id) do + encrypt_with_public_key(data, public_key) + end + end + + def encrypt(data, actor) do + with {:ok, public_key} <- Keys.public_key_from_data(actor) do + encrypt_with_public_key(data, public_key) + end + end + + @doc """ + Decrypts data for a given actor using their private key. + + ## Parameters + - encrypted_data: The data to decrypt (binary) + - actor: The Actor struct with private keys + + ## Returns + - {:ok, decrypted_data} on success + - {:error, reason} on failure + """ + def decrypt(encrypted_data, %Actor{local: true, keys: keys} = actor) when not is_nil(keys) do + with {:ok, private_key, _public_key} <- Keys.keypair_from_pem(keys) do + decrypt_with_private_key(encrypted_data, private_key) + end + end + + def decrypt(encrypted_data, ap_id) when is_binary(ap_id) do + with {:ok, actor} <- Actor.get_cached(ap_id: ap_id) do + decrypt(encrypted_data, actor) + end + end + + def decrypt(_encrypted_data, %Actor{local: false} = actor) do + error(actor, "Cannot perform decryption for remote actors") + end + + def decrypt(_encrypted_data, actor) do + error(actor, "Could not find a private key to use for decryption") + end + + # Private functions + + defp encrypt_with_public_key(data, public_key) do + with {:ok, decoded_key} <- Keys.public_key_decode(public_key) do + encrypted = :public_key.encrypt_public(data, decoded_key) + {:ok, encrypted} + end + rescue + e -> error(e) + end + + defp decrypt_with_private_key(encrypted_data, private_key) do + decrypted = :public_key.decrypt_private(encrypted_data, private_key) + {:ok, decrypted} + rescue + e in ErlangError -> + case e do + %ErlangError{original: {:error, _, msg}} -> + error(to_string(msg)) + + _ -> + error(e) + end + + e -> + error(e) + end +end diff --git a/lib/safety/signatures.ex b/lib/safety/http_signatures_adapter.ex similarity index 98% rename from lib/safety/signatures.ex rename to lib/safety/http_signatures_adapter.ex index ac67888..26cf850 100644 --- a/lib/safety/signatures.ex +++ b/lib/safety/http_signatures_adapter.ex @@ -1,4 +1,4 @@ -defmodule ActivityPub.Safety.Signatures do +defmodule ActivityPub.Safety.HTTP.Signatures do @moduledoc """ Implementation for behaviour from `HTTPSignatures` library """ diff --git a/lib/safety/keys.ex b/lib/safety/keys.ex index f37eb32..a18b83f 100644 --- a/lib/safety/keys.ex +++ b/lib/safety/keys.ex @@ -10,7 +10,7 @@ defmodule ActivityPub.Safety.Keys do alias ActivityPub.Actor alias ActivityPub.Utils alias ActivityPub.Safety.Keys - # alias ActivityPub.Safety.Signatures + # alias ActivityPub.Safety.HTTP.Signatures # alias ActivityPub.Federator.Fetcher alias ActivityPub.Federator.Adapter @@ -122,7 +122,7 @@ defmodule ActivityPub.Safety.Keys do with {:ok, pem} <- generate_rsa_pem(), {:ok, actor} <- Adapter.update_local_actor(actor, %{keys: pem}), - {:ok, actor} <- Actor.set_cache(actor) do + {:ok, actor} <- Actor.set_cache(actor) |> debug("donz") do {:ok, actor} else e -> error(e, "Could not generate or save keys") diff --git a/test/activity_pub/data/encryption_test.exs b/test/activity_pub/data/encryption_test.exs new file mode 100644 index 0000000..867481f --- /dev/null +++ b/test/activity_pub/data/encryption_test.exs @@ -0,0 +1,82 @@ +defmodule ActivityPub.Safety.EncryptionTest do + # this isn't used but simply a proof of concept + use ActivityPub.DataCase, async: false + alias ActivityPub.Safety.Encryption + alias ActivityPub.Safety.Keys + alias ActivityPub.Actor + + # Import the test helpers that contain the local_actor() function + import ActivityPub.Factory + + describe "encrypt/2" do + test "successfully encrypts data for a local actor" do + actor = local_actor() + {:ok, actor} = Keys.ensure_keys_present(actor.actor) + + assert {:ok, encrypted} = Encryption.encrypt("secret message", actor) + assert is_binary(encrypted) + assert encrypted != "secret message" + end + + test "returns error for actor without public key" do + actor = local_actor() + + actor = %{actor | data: Map.delete(actor.data, "publicKey")} + + assert {:error, "Public key not found"} = Encryption.encrypt("secret message", actor) + end + + test "encrypts data using AP ID" do + actor = local_actor() + {:ok, actor} = Keys.ensure_keys_present(actor.actor) + + assert {:ok, encrypted} = Encryption.encrypt("secret message", actor.data["id"]) + assert is_binary(encrypted) + assert encrypted != "secret message" + end + end + + describe "decrypt/2" do + test "successfully decrypts data for a local actor" do + actor = local_actor() + {:ok, actor} = Keys.ensure_keys_present(actor.actor) + + assert {:ok, encrypted} = Encryption.encrypt("secret message", actor) + assert encrypted != "secret message" + assert {:ok, "secret message"} = Encryption.decrypt(encrypted, actor) + end + + test "fails decryption for remote actor" do + actor = local_actor() + {:ok, actor} = Keys.ensure_keys_present(actor.actor) + remote_actor = %{actor | local: false} + + assert {:error, "Cannot perform decryption for remote actors"} = + Encryption.decrypt("fake encrypted data", remote_actor) + end + + test "fails decryption with invalid data" do + actor = local_actor() + {:ok, actor} = Keys.ensure_keys_present(actor.actor) + + assert {:error, _} = Encryption.decrypt("non-encrypted data", actor) + end + + test "fails decryption with someone else's keys" do + actor = local_actor() + {:ok, actor} = Keys.ensure_keys_present(actor.actor) + + actor2 = local_actor() + {:ok, actor2} = Keys.ensure_keys_present(actor2.actor) + + assert {:ok, encrypted} = Encryption.encrypt("secret message", actor) + assert {:error, _} = Encryption.decrypt(encrypted, actor2) + end + + test "fails decryption with missing keys" do + actor = local_actor().actor + + assert {:error, _} = Encryption.decrypt("non-encrypted data", actor) + end + end +end diff --git a/test/activity_pub/safety/signatures/mapped_signature_to_identity_plug_test.exs b/test/activity_pub/safety/signatures/mapped_signature_to_identity_plug_test.exs index 3cb32ca..7bb5860 100644 --- a/test/activity_pub/safety/signatures/mapped_signature_to_identity_plug_test.exs +++ b/test/activity_pub/safety/signatures/mapped_signature_to_identity_plug_test.exs @@ -6,7 +6,7 @@ defmodule ActivityPub.Web.Plugs.MappedSignatureToIdentityPlugTest do use ActivityPub.Web.ConnCase, async: false alias ActivityPub.Web.Plugs.MappedSignatureToIdentityPlug alias ActivityPub.Config - alias ActivityPub.Safety.Signatures + alias ActivityPub.Safety.HTTP.Signatures import Tesla.Mock import Plug.Conn diff --git a/test/activity_pub/safety/signatures/signature_test.exs b/test/activity_pub/safety/signatures/signature_test.exs index 9bb10ce..7fb0b5f 100644 --- a/test/activity_pub/safety/signatures/signature_test.exs +++ b/test/activity_pub/safety/signatures/signature_test.exs @@ -11,7 +11,7 @@ defmodule ActivityPub.Safety.SignatureTest do alias ActivityPub.Actor alias ActivityPub.Safety.Keys alias ActivityPub.Utils - alias ActivityPub.Safety.Signatures + alias ActivityPub.Safety.HTTP.Signatures alias ActivityPub.Federator.Fetcher @private_key "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEA48qb4v6kqigZutO9Ot0wkp27GIF2LiVaADgxQORZozZR63jH\nTaoOrS3Xhngbgc8SSOhfXET3omzeCLqaLNfXnZ8OXmuhJfJSU6mPUvmZ9QdT332j\nfN/g3iWGhYMf/M9ftCKh96nvFVO/tMruzS9xx7tkrfJjehdxh/3LlJMMImPtwcD7\nkFXwyt1qZTAU6Si4oQAJxRDQXHp1ttLl3Ob829VM7IKkrVmY8TD+JSlV0jtVJPj6\n1J19ytKTx/7UaucYvb9HIiBpkuiy5n/irDqKLVf5QEdZoNCdojOZlKJmTLqHhzKP\n3E9TxsUjhrf4/EqegNc/j982RvOxeu4i40zMQwIDAQABAoIBAQDH5DXjfh21i7b4\ncXJuw0cqget617CDUhemdakTDs9yH+rHPZd3mbGDWuT0hVVuFe4vuGpmJ8c+61X0\nRvugOlBlavxK8xvYlsqTzAmPgKUPljyNtEzQ+gz0I+3mH2jkin2rL3D+SksZZgKm\nfiYMPIQWB2WUF04gB46DDb2mRVuymGHyBOQjIx3WC0KW2mzfoFUFRlZEF+Nt8Ilw\nT+g/u0aZ1IWoszbsVFOEdghgZET0HEarum0B2Je/ozcPYtwmU10iBANGMKdLqaP/\nj954BPunrUf6gmlnLZKIKklJj0advx0NA+cL79+zeVB3zexRYSA5o9q0WPhiuTwR\n/aedWHnBAoGBAP0sDWBAM1Y4TRAf8ZI9PcztwLyHPzfEIqzbObJJnx1icUMt7BWi\n+/RMOnhrlPGE1kMhOqSxvXYN3u+eSmWTqai2sSH5Hdw2EqnrISSTnwNUPINX7fHH\njEkgmXQ6ixE48SuBZnb4w1EjdB/BA6/sjL+FNhggOc87tizLTkMXmMtTAoGBAOZV\n+wPuAMBDBXmbmxCuDIjoVmgSlgeRunB1SA8RCPAFAiUo3+/zEgzW2Oz8kgI+xVwM\n33XkLKrWG1Orhpp6Hm57MjIc5MG+zF4/YRDpE/KNG9qU1tiz0UD5hOpIU9pP4bR/\ngxgPxZzvbk4h5BfHWLpjlk8UUpgk6uxqfti48c1RAoGBALBOKDZ6HwYRCSGMjUcg\n3NPEUi84JD8qmFc2B7Tv7h2he2ykIz9iFAGpwCIyETQsJKX1Ewi0OlNnD3RhEEAy\nl7jFGQ+mkzPSeCbadmcpYlgIJmf1KN/x7fDTAepeBpCEzfZVE80QKbxsaybd3Dp8\nCfwpwWUFtBxr4c7J+gNhAGe/AoGAPn8ZyqkrPv9wXtyfqFjxQbx4pWhVmNwrkBPi\nZ2Qh3q4dNOPwTvTO8vjghvzIyR8rAZzkjOJKVFgftgYWUZfM5gE7T2mTkBYq8W+U\n8LetF+S9qAM2gDnaDx0kuUTCq7t87DKk6URuQ/SbI0wCzYjjRD99KxvChVGPBHKo\n1DjqMuECgYEAgJGNm7/lJCS2wk81whfy/ttKGsEIkyhPFYQmdGzSYC5aDc2gp1R3\nxtOkYEvdjfaLfDGEa4UX8CHHF+w3t9u8hBtcdhMH6GYb9iv6z0VBTt4A/11HUR49\n3Z7TQ18Iyh3jAUCzFV9IJlLIExq5Y7P4B3ojWFBN607sDCt8BMPbDYs=\n-----END RSA PRIVATE KEY-----" diff --git a/test/test_helper.exs b/test/test_helper.exs index 093155d..55a0746 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,4 +1,4 @@ -import ActivityPub.Test.Helpers +# import ActivityPub.Test.Helpers import ActivityPub.Utils {:ok, _} = Application.ensure_all_started(:ex_machina)