Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
include:
- elixir: "1.18"
otp: "27"
latest: true
- elixir: "1.16"
otp: "26"
- elixir: "1.15"
Expand Down Expand Up @@ -54,6 +55,7 @@ jobs:
run: mix deps.get

- name: Check Formatting
if: matrix.latest
run: mix format --check-formatted

- name: Run unit tests
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## v0.6.4

### Enhancements

* Implementation of User ID Packet (Tag 13) - `OpenPGP.UserIdPacket`
* Add `OpenPGP.Encode` protocol implementation for:
* `OpenPGP.UserIdPacket`
* `OpenPGP.PublicKeyPacket`
* Better error message for:
* `OpenPGP.cast_packet/1`

## v0.6.3

### Enhancements
Expand Down
13 changes: 12 additions & 1 deletion lib/open_pgp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ defmodule OpenPGP do
| %OpenPGP.PublicKeyEncryptedSessionKeyPacket{}
| %OpenPGP.SecretKeyPacket{}
| %OpenPGP.PublicKeyPacket{}
| %OpenPGP.UserIdPacket{}
| %OpenPGP.CompressedDataPacket{}
| %OpenPGP.IntegrityProtectedDataPacket{}
| %OpenPGP.LiteralDataPacket{}
Expand Down Expand Up @@ -125,6 +126,7 @@ defmodule OpenPGP do
7 => OpenPGP.SecretKeyPacket,
8 => OpenPGP.CompressedDataPacket,
11 => OpenPGP.LiteralDataPacket,
13 => OpenPGP.UserIdPacket,
14 => OpenPGP.PublicKeyPacket,
18 => OpenPGP.IntegrityProtectedDataPacket
}
Expand All @@ -141,7 +143,16 @@ defmodule OpenPGP do
case packet.tag do
%PacketTag{tag: {tag_id, _}} when tag_id in @tag_to_packet_ids ->
impl = Map.get(@tag_to_packet, tag_id)
{casted, <<>>} = packet |> Util.concat_body() |> impl.decode()
{casted, rest} = packet |> Util.concat_body() |> impl.decode()
rsize = byte_size(rest)

if rsize > 0 do
err =
"#{inspect(impl)}.decode/1 decoded #{rsize} unexpected trailing byte(s): #{inspect(rest, printable_limit: 20)}"

raise(err)
end

casted

_ ->
Expand Down
34 changes: 34 additions & 0 deletions lib/open_pgp/encode/public_key_packet_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defimpl OpenPGP.Encode, for: OpenPGP.PublicKeyPacket do
alias OpenPGP.PublicKeyPacket
alias OpenPGP.Util

def tag(_), do: {6, "Public-Key Packet"}

@doc """
Encode Public-Key Packet.
Return encoded packet body (version 4 only).

### Example:

iex> packet = %OpenPGP.PublicKeyPacket{
...> version: 4,
...> created_at: ~U[2024-01-02 18:03:04Z],
...> algo: {1, "RSA (Encrypt or Sign) [HAC]"},
...> material: {<<0x01>>, <<0x01>>}
...> }
iex> OpenPGP.Encode.encode(packet)
<<4::8, 1704218584::32, 1::8, 0, 1, 1, 0, 1, 1>>

"""
def encode(%PublicKeyPacket{version: 4} = packet, _opts) do
{algo_id, _} = packet.algo
ts = DateTime.to_unix(packet.created_at)

encoded_material =
for mpi <- Tuple.to_list(packet.material), reduce: "" do
acc -> acc <> Util.encode_mpi(mpi)
end

<<4::8, ts::32, algo_id::8, encoded_material::binary>>
end
end
15 changes: 15 additions & 0 deletions lib/open_pgp/encode/user_id_packet_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defimpl OpenPGP.Encode, for: OpenPGP.UserIdPacket do
def tag(_), do: {13, "User ID Packet"}

@doc """
Encode User ID Packet.
Return encoded packet body (the UTF-8 User ID string).

### Example:

iex> OpenPGP.Encode.encode(%OpenPGP.UserIdPacket{id: "Alice <alice@example.com>"})
"Alice <alice@example.com>"

"""
def encode(%OpenPGP.UserIdPacket{id: id}, _opts), do: id
end
2 changes: 0 additions & 2 deletions lib/open_pgp/secret_key_packet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ defmodule OpenPGP.SecretKeyPacket do

@behaviour OpenPGP.Packet.Behaviour

alias OpenPGP.Util

defstruct [
:public_key,
:s2k_usage,
Expand Down
36 changes: 36 additions & 0 deletions lib/open_pgp/user_id_packet.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
defmodule OpenPGP.UserIdPacket do
@moduledoc """
Represents structured data for User ID Packet (Tag 13).

### Example:

iex> OpenPGP.UserIdPacket.decode("Alice <alice@example.com>")
{%OpenPGP.UserIdPacket{id: "Alice <alice@example.com>"}, <<>>}

---

## [RFC4880](https://www.ietf.org/rfc/rfc4880.txt)

### 5.11. User ID Packet (Tag 13)

A User ID packet consists of UTF-8 text that is intended to represent
the name and email address of the key holder. By convention, it
includes an RFC 2822 mail name-addr, but there are no restrictions on
its content. The packet length in the header specifies the length of
the User ID.
"""

@behaviour OpenPGP.Packet.Behaviour

defstruct [:id]

@type t :: %__MODULE__{id: binary()}

@doc """
Decode User ID Packet given input binary.
Return structured packet and remaining binary (empty binary).
"""
@impl OpenPGP.Packet.Behaviour
@spec decode(binary()) :: {t(), <<>>}
def decode("" <> _ = input), do: {%__MODULE__{id: input}, ""}
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule OpenPGP.MixProject do
use Mix.Project

@source_url "https://github.com/DivvyPayHQ/open_pgp"
@version "0.6.3"
@version "0.6.4"
@description "OpenPGP Message Format in Elixir - RFC4880"

def project() do
Expand Down
36 changes: 36 additions & 0 deletions test/open_pgp/public_key_packet_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
defmodule OpenPGP.PublicKeyPacketTest do
use OpenPGP.Test.Case, async: true

doctest OpenPGP.Encode.impl_for!(%OpenPGP.PublicKeyPacket{})

alias OpenPGP.Packet
alias OpenPGP.Packet.PacketTag
alias OpenPGP.PublicKeyPacket
Expand Down Expand Up @@ -70,4 +73,37 @@ defmodule OpenPGP.PublicKeyPacketTest do
"2CAC73CCC53706BE856E26D02D0A7F1AB8122614E91000F5EB1B240F50C66D9048F861D3" ==
Base.encode16(value_y)
end

describe "OpenPGP.Encode.encode/1,2" do
test "encodes RSA public key to original bytes" do
assert [%Packet{body: chunks, tag: %PacketTag{tag: {5, "Secret-Key Packet"}}} | _] =
OpenPGP.list_packets(@rsa2048_priv)

# The Secret-Key packet contain all the data of the Public-Key packet,
# with additional algorithm-specific secret-key data appended.
# Here we decode a Secret-Key packet with the Publick-Key decoder,
# which will return the PublicKeyPacket struct and the remaining
# algorithm-specific secret-key data as a binary.
data = OpenPGP.Util.concat_body(chunks)
{pk_decoded, rest} = PublicKeyPacket.decode(data)

pk_raw_length = byte_size(data) - byte_size(rest)
<<pk_raw::bytes-size(pk_raw_length), _::binary>> = data

assert OpenPGP.Encode.encode(pk_decoded) == pk_raw
end

test "encodes ElGamal public key to original bytes" do
assert %Packet{body: chunks, tag: %PacketTag{tag: {14, "Public-Subkey Packet"}}} =
@elg2048_pub |> OpenPGP.list_packets() |> Enum.at(3)

data = OpenPGP.Util.concat_body(chunks)
{pk_decoded, <<>>} = PublicKeyPacket.decode(data)
assert OpenPGP.Encode.encode(pk_decoded) == data
end

test "tag/1 returns {6, Public-Key Packet}" do
assert {6, "Public-Key Packet"} = OpenPGP.Encode.tag(%PublicKeyPacket{})
end
end
end
2 changes: 1 addition & 1 deletion test/open_pgp/radix64_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ defmodule OpenPGP.Radix64Test do
version: 4
}
},
%Packet{tag: %PacketTag{tag: {13, "User ID Packet"}}},
%OpenPGP.UserIdPacket{},
%Packet{tag: %PacketTag{tag: {2, "Signature Packet"}}},
%OpenPGP.SecretKeyPacket{
public_key: %PublicKeyPacket{
Expand Down
32 changes: 32 additions & 0 deletions test/open_pgp/user_id_packet_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule OpenPGP.UserIdPacketTest do
use OpenPGP.Test.Case, async: true
doctest OpenPGP.UserIdPacket
doctest OpenPGP.Encode.impl_for!(%OpenPGP.UserIdPacket{})

alias OpenPGP.Encode
alias OpenPGP.UserIdPacket

test "decode/1 returns the input as id with empty remainder" do
assert {%UserIdPacket{id: "John Doe <john@example.com>"}, <<>>} =
UserIdPacket.decode("John Doe <john@example.com>")
end

test "cast_packets casts Tag 13 to UserIdPacket" do
message = File.read!("test/fixtures/rsa2048-priv.pgp")
[_sk, uid_pkt | _] = OpenPGP.list_packets(message)

assert [%UserIdPacket{id: "John Doe (RSA2048) <john.doe@example.com>"}] = OpenPGP.cast_packets([uid_pkt])
end

describe "OpenPGP.Encode.encode/1,2" do
test "encode/2 returns the id binary" do
packet = %UserIdPacket{id: "Alice <alice@example.com>"}
assert Encode.encode(packet) == "Alice <alice@example.com>"
end

test "tag/1 returns packet tag 13" do
packet = %UserIdPacket{id: "Alice <alice@example.com>"}
assert {13, "User ID Packet"} == Encode.tag(packet)
end
end
end
24 changes: 22 additions & 2 deletions test/open_pgp_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,30 @@ defmodule OpenPGPTest do
end
end

@expected_error "OpenPGP.PublicKeyPacket.decode/1 decoded 697 unexpected trailing byte(s): " <>
"<<254, 7, 3, 2, 248, 49, 205, 223, 27, 66, 166, 109, 252, 54, 235, 29, 19, 66, " <>
"217, 249, 233, 73, 143, 69, 142, 10, 18, 42, 106, 122, 114, 71, 167, 145, 111, " <>
"190, 206, 40, 102, 166, 213, 241, 148, 116, 163, 167, 163, 228, 5, 223, ...>>"
test "cast_packet/1 raises on trailing bytes from decoder" do
# PublicKeyPacket.decode/1 stops after reading the key material MPIs,
# so a Tag 6 packet body with extra trailing bytes produces a remainder.
# The secret key body starts with the public key fields followed by secret key data.
# Casting it as a Tag 6 (Public-Key) packet will leave the secret key bytes as remainder.

[%Packet{body: chunks, tag: %PacketTag{tag: {5, "Secret-Key Packet"}}} | _] = OpenPGP.list_packets(@rsa2048_priv)

ptag = %PacketTag{format: :new, tag: {6, "Public-Key Packet"}}
packet = %Packet{tag: ptag, body: chunks}

assert_raise RuntimeError, @expected_error, fn ->
OpenPGP.cast_packet(packet)
end
end

test "decode secret key message" do
assert [
%SecretKeyPacket{},
%Packet{tag: %PacketTag{tag: {13, "User ID Packet"}}},
%OpenPGP.UserIdPacket{},
%Packet{tag: %PacketTag{tag: {2, "Signature Packet"}}},
%SecretKeyPacket{},
%Packet{tag: %PacketTag{tag: {2, "Signature Packet"}}}
Expand Down Expand Up @@ -194,7 +214,7 @@ defmodule OpenPGPTest do
assert keyring =
[
%SecretKeyPacket{},
%Packet{tag: %PacketTag{tag: {13, "User ID Packet"}}},
%OpenPGP.UserIdPacket{},
%Packet{tag: %PacketTag{tag: {2, "Signature Packet"}}},
%SecretKeyPacket{},
%Packet{tag: %PacketTag{tag: {2, "Signature Packet"}}}
Expand Down
Loading