Skip to content

Commit 978f1d0

Browse files
committed
proof of concept for bonfire-networks/bonfire-app#1048
1 parent 57fff8e commit 978f1d0

7 files changed

+181
-6
lines changed

lib/safety/encryption.ex

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
defmodule ActivityPub.Safety.Encryption do
2+
@moduledoc """
3+
Provides encryption and decryption functionality using RSA keys managed by ActivityPub.Safety.Keys.
4+
5+
NOTE: not used at the moment, simply intended as a proof-of-concept
6+
"""
7+
import Untangle
8+
9+
alias ActivityPub.Safety.Keys
10+
alias ActivityPub.Actor
11+
12+
@doc """
13+
Encrypts data for a given actor using their public key.
14+
15+
## Parameters
16+
- data: The data to encrypt (binary or string)
17+
- actor: The Actor struct or AP ID of the recipient
18+
19+
## Returns
20+
- {:ok, encrypted_data} on success
21+
- {:error, reason} on failure
22+
"""
23+
def encrypt(data, ap_id) when is_binary(ap_id) do
24+
with {:ok, public_key} <- Keys.get_public_key_for_ap_id(ap_id) do
25+
encrypt_with_public_key(data, public_key)
26+
end
27+
end
28+
29+
def encrypt(data, actor) do
30+
with {:ok, public_key} <- Keys.public_key_from_data(actor) do
31+
encrypt_with_public_key(data, public_key)
32+
end
33+
end
34+
35+
@doc """
36+
Decrypts data for a given actor using their private key.
37+
38+
## Parameters
39+
- encrypted_data: The data to decrypt (binary)
40+
- actor: The Actor struct with private keys
41+
42+
## Returns
43+
- {:ok, decrypted_data} on success
44+
- {:error, reason} on failure
45+
"""
46+
def decrypt(encrypted_data, %Actor{local: true, keys: keys} = actor) when not is_nil(keys) do
47+
with {:ok, private_key, _public_key} <- Keys.keypair_from_pem(keys) do
48+
decrypt_with_private_key(encrypted_data, private_key)
49+
end
50+
end
51+
52+
def decrypt(encrypted_data, ap_id) when is_binary(ap_id) do
53+
with {:ok, actor} <- Actor.get_cached(ap_id: ap_id) do
54+
decrypt(encrypted_data, actor)
55+
end
56+
end
57+
58+
def decrypt(_encrypted_data, %Actor{local: false} = actor) do
59+
error(actor, "Cannot perform decryption for remote actors")
60+
end
61+
62+
def decrypt(_encrypted_data, actor) do
63+
error(actor, "Could not find a private key to use for decryption")
64+
end
65+
66+
# Private functions
67+
68+
defp encrypt_with_public_key(data, public_key) do
69+
with {:ok, decoded_key} <- Keys.public_key_decode(public_key) do
70+
encrypted = :public_key.encrypt_public(data, decoded_key)
71+
{:ok, encrypted}
72+
end
73+
rescue
74+
e -> error(e)
75+
end
76+
77+
defp decrypt_with_private_key(encrypted_data, private_key) do
78+
decrypted = :public_key.decrypt_private(encrypted_data, private_key)
79+
{:ok, decrypted}
80+
rescue
81+
e in ErlangError ->
82+
case e do
83+
%ErlangError{original: {:error, _, msg}} ->
84+
error(to_string(msg))
85+
86+
_ ->
87+
error(e)
88+
end
89+
90+
e ->
91+
error(e)
92+
end
93+
end

lib/safety/signatures.ex lib/safety/http_signatures_adapter.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule ActivityPub.Safety.Signatures do
1+
defmodule ActivityPub.Safety.HTTP.Signatures do
22
@moduledoc """
33
Implementation for behaviour from `HTTPSignatures` library
44
"""

lib/safety/keys.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ defmodule ActivityPub.Safety.Keys do
1010
alias ActivityPub.Actor
1111
alias ActivityPub.Utils
1212
alias ActivityPub.Safety.Keys
13-
# alias ActivityPub.Safety.Signatures
13+
# alias ActivityPub.Safety.HTTP.Signatures
1414
# alias ActivityPub.Federator.Fetcher
1515
alias ActivityPub.Federator.Adapter
1616

@@ -122,7 +122,7 @@ defmodule ActivityPub.Safety.Keys do
122122

123123
with {:ok, pem} <- generate_rsa_pem(),
124124
{:ok, actor} <- Adapter.update_local_actor(actor, %{keys: pem}),
125-
{:ok, actor} <- Actor.set_cache(actor) do
125+
{:ok, actor} <- Actor.set_cache(actor) |> debug("donz") do
126126
{:ok, actor}
127127
else
128128
e -> error(e, "Could not generate or save keys")
+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
defmodule ActivityPub.Safety.EncryptionTest do
2+
# this isn't used but simply a proof of concept
3+
use ActivityPub.DataCase, async: false
4+
alias ActivityPub.Safety.Encryption
5+
alias ActivityPub.Safety.Keys
6+
alias ActivityPub.Actor
7+
8+
# Import the test helpers that contain the local_actor() function
9+
import ActivityPub.Factory
10+
11+
describe "encrypt/2" do
12+
test "successfully encrypts data for a local actor" do
13+
actor = local_actor()
14+
{:ok, actor} = Keys.ensure_keys_present(actor.actor)
15+
16+
assert {:ok, encrypted} = Encryption.encrypt("secret message", actor)
17+
assert is_binary(encrypted)
18+
assert encrypted != "secret message"
19+
end
20+
21+
test "returns error for actor without public key" do
22+
actor = local_actor()
23+
24+
actor = %{actor | data: Map.delete(actor.data, "publicKey")}
25+
26+
assert {:error, "Public key not found"} = Encryption.encrypt("secret message", actor)
27+
end
28+
29+
test "encrypts data using AP ID" do
30+
actor = local_actor()
31+
{:ok, actor} = Keys.ensure_keys_present(actor.actor)
32+
33+
assert {:ok, encrypted} = Encryption.encrypt("secret message", actor.data["id"])
34+
assert is_binary(encrypted)
35+
assert encrypted != "secret message"
36+
end
37+
end
38+
39+
describe "decrypt/2" do
40+
test "successfully decrypts data for a local actor" do
41+
actor = local_actor()
42+
{:ok, actor} = Keys.ensure_keys_present(actor.actor)
43+
44+
assert {:ok, encrypted} = Encryption.encrypt("secret message", actor)
45+
assert encrypted != "secret message"
46+
assert {:ok, "secret message"} = Encryption.decrypt(encrypted, actor)
47+
end
48+
49+
test "fails decryption for remote actor" do
50+
actor = local_actor()
51+
{:ok, actor} = Keys.ensure_keys_present(actor.actor)
52+
remote_actor = %{actor | local: false}
53+
54+
assert {:error, "Cannot perform decryption for remote actors"} =
55+
Encryption.decrypt("fake encrypted data", remote_actor)
56+
end
57+
58+
test "fails decryption with invalid data" do
59+
actor = local_actor()
60+
{:ok, actor} = Keys.ensure_keys_present(actor.actor)
61+
62+
assert {:error, _} = Encryption.decrypt("non-encrypted data", actor)
63+
end
64+
65+
test "fails decryption with someone else's keys" do
66+
actor = local_actor()
67+
{:ok, actor} = Keys.ensure_keys_present(actor.actor)
68+
69+
actor2 = local_actor()
70+
{:ok, actor2} = Keys.ensure_keys_present(actor2.actor)
71+
72+
assert {:ok, encrypted} = Encryption.encrypt("secret message", actor)
73+
assert {:error, _} = Encryption.decrypt(encrypted, actor2)
74+
end
75+
76+
test "fails decryption with missing keys" do
77+
actor = local_actor().actor
78+
79+
assert {:error, _} = Encryption.decrypt("non-encrypted data", actor)
80+
end
81+
end
82+
end

test/activity_pub/safety/signatures/mapped_signature_to_identity_plug_test.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule ActivityPub.Web.Plugs.MappedSignatureToIdentityPlugTest do
66
use ActivityPub.Web.ConnCase, async: false
77
alias ActivityPub.Web.Plugs.MappedSignatureToIdentityPlug
88
alias ActivityPub.Config
9-
alias ActivityPub.Safety.Signatures
9+
alias ActivityPub.Safety.HTTP.Signatures
1010

1111
import Tesla.Mock
1212
import Plug.Conn

test/activity_pub/safety/signatures/signature_test.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule ActivityPub.Safety.SignatureTest do
1111
alias ActivityPub.Actor
1212
alias ActivityPub.Safety.Keys
1313
alias ActivityPub.Utils
14-
alias ActivityPub.Safety.Signatures
14+
alias ActivityPub.Safety.HTTP.Signatures
1515
alias ActivityPub.Federator.Fetcher
1616

1717
@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-----"

test/test_helper.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import ActivityPub.Test.Helpers
1+
# import ActivityPub.Test.Helpers
22
import ActivityPub.Utils
33

44
{:ok, _} = Application.ensure_all_started(:ex_machina)

0 commit comments

Comments
 (0)