Skip to content

Commit ed22345

Browse files
committed
feat: add encrypt
1 parent cf81444 commit ed22345

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

lib/web_push_elixir.ex

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,79 @@
11
defmodule WebPushElixir do
22
require Logger
33

4+
@auth_info "Content-Encoding: auth" <> <<0>>
5+
@one_buffer <<1>>
6+
47
def gen_keypair do
58
{public, private} = :crypto.generate_key(:ecdh, :prime256v1)
69

710
fn ->
811
Logger.info(%{:public_key => Base.url_encode64(public, padding: false)})
912
Logger.info(%{:private_key => Base.url_encode64(private, padding: false)})
13+
1014
Logger.info(%{:subject => "mailto:[email protected]"})
1115
end
1216
end
17+
18+
def encrypt(message, subscription) do
19+
client_public_key = Base.url_decode64!(subscription.keys.p256dh, padding: false)
20+
client_auth_secret = Base.url_decode64!(subscription.keys.auth, padding: false)
21+
22+
salt = :crypto.strong_rand_bytes(16)
23+
24+
{server_public_key, server_private_key} = :crypto.generate_key(:ecdh, :prime256v1)
25+
26+
shared_secret = :crypto.compute_key(:ecdh, client_public_key, server_private_key, :prime256v1)
27+
28+
prk = hkdf(client_auth_secret, shared_secret, @auth_info, 32)
29+
30+
context = create_context(client_public_key, server_public_key)
31+
32+
content_encryption_key_info = create_info("aesgcm", context)
33+
content_encryption_key = hkdf(salt, prk, content_encryption_key_info, 16)
34+
35+
nonce_info = create_info("nonce", context)
36+
nonce = hkdf(salt, prk, nonce_info, 12)
37+
38+
ciphertext = encrypt_payload(message, content_encryption_key, nonce)
39+
40+
%{ciphertext: ciphertext, salt: salt, server_public_key: server_public_key}
41+
end
42+
43+
defp hkdf(salt, ikm, info, length) do
44+
prk =
45+
:crypto.mac_init(:hmac, :sha256, salt)
46+
|> :crypto.mac_update(ikm)
47+
|> :crypto.mac_final()
48+
49+
:crypto.mac_init(:hmac, :sha256, prk)
50+
|> :crypto.mac_update(info)
51+
|> :crypto.mac_update(@one_buffer)
52+
|> :crypto.mac_final()
53+
|> :binary.part(0, length)
54+
end
55+
56+
defp create_context(client_public_key, server_public_key) do
57+
<<0, byte_size(client_public_key)::unsigned-big-integer-size(16)>> <>
58+
client_public_key <>
59+
<<byte_size(server_public_key)::unsigned-big-integer-size(16)>> <> server_public_key
60+
end
61+
62+
defp create_info(type, context) do
63+
"Content-Encoding: " <> type <> <<0>> <> "P-256" <> context
64+
end
65+
66+
defp encrypt_payload(plaintext, content_encryption_key, nonce) do
67+
{cipher_text, cipher_tag} =
68+
:crypto.crypto_one_time_aead(
69+
:aes_128_gcm,
70+
content_encryption_key,
71+
nonce,
72+
plaintext,
73+
"",
74+
true
75+
)
76+
77+
cipher_text <> cipher_tag
78+
end
1379
end

test/web_push_elixir_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,37 @@ defmodule WebPushElixirTest do
33

44
import ExUnit.CaptureLog
55

6+
@subscription_from_client '{"endpoint":"https://some.pushservice.com/something-unique","keys":{"p256dh":"BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=","auth":"FPssNDTKnInHVndSTdbKFw=="}}'
7+
@subscription_decoded %{
8+
endpoint: "https://some.pushservice.com/something-unique",
9+
keys: %{
10+
auth: "FPssNDTKnInHVndSTdbKFw==",
11+
p256dh:
12+
"BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI="
13+
}
14+
}
15+
16+
@salt_length 16
17+
@server_public_key_length 65
18+
619
test "it should gen keypair" do
720
assert capture_log(WebPushElixir.gen_keypair()) =~ "public_key:"
821
assert capture_log(WebPushElixir.gen_keypair()) =~ "private_key:"
922
assert capture_log(WebPushElixir.gen_keypair()) =~ "subject:"
1023
assert capture_log(WebPushElixir.gen_keypair()) =~ "mailto:[email protected]"
1124
end
25+
26+
test "it should decode" do
27+
assert Jason.decode!(@subscription_from_client, keys: :atoms) == @subscription_decoded
28+
end
29+
30+
test "it should encrypt" do
31+
response = WebPushElixir.encrypt("some message", @subscription_decoded)
32+
33+
assert is_binary(response.ciphertext)
34+
assert is_binary(response.salt)
35+
assert byte_size(response.salt) == @salt_length
36+
assert is_binary(response.server_public_key)
37+
assert byte_size(response.server_public_key) == @server_public_key_length
38+
end
1239
end

0 commit comments

Comments
 (0)