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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# Changelog

## v0.6.0

### Enhancements

* Introduced `OpenPGP.Encode` protocol with `.encode/1,2` and `.tag/1`.
* Add `OpenPGP.Encode` protocol implementation for:
* `OpenPGP.PublicKeyEncryptedSessionKeyPacket`
* `OpenPGP.IntegrityProtectedDataPacket`
* `OpenPGP.LiteralDataPacket`
* `OpenPGP.Packet`
* `OpenPGP.Packet.PacketTag`
* `OpenPGP.Packet.BodyChunk`
* Introduced `OpenPGP.Encrypt` protocol with `.encrypt/1,2`.
* Add `OpenPGP.Encrypt` protocol implementation for:
* `OpenPGP.PublicKeyEncryptedSessionKeyPacket` with Elgamal (Public-Key algo 16).
* `OpenPGP.IntegrityProtectedDataPacket` with AES-128, AES-192, AES-256 (Sym.algo 7,8,9).
* Added `OpenPGP.encode_packet/1` that delegate to `OpenPGP.Encode` protocol.
* Added `OpenPGP.encrypt_packet/1,2` that delegate to `OpenPGP.Encrypt` protocol.
* Add ElGamal algorithm support to `OpenPGP.PublicKeyPacket.decode/1`.
* Introduced `OpenPGP.ModificationDetectionCodePacket`.
* Introduced `OpenPGP.Util.PKCS1` with PKCS#1 block encoding EME-PKCS1-v1_5.
* Refactored `OpenPGP.Util.encode_mpi/1` and added exception for too long big-endian numbers (>65535 octets).
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ Packet specific decoders implement `OpenPGP.Packet.Behaviour`, which exposes `.d

Usage example of a comon use case can be found in `test/open_pgp/open_pgp_test.exs` in the test **"full integration: load private key and decrypt encrypted file"**

As of v0.6.x

1. Limited support of encoding and encryption (see CHANGELOG.md) for details.

## Refs, Snippets, Misc

```console
Expand Down
31 changes: 24 additions & 7 deletions lib/open_pgp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,21 @@ defmodule OpenPGP do
]
"""

alias __MODULE__.Encode
alias __MODULE__.Encrypt
alias __MODULE__.Packet
alias __MODULE__.Packet.PacketTag
alias __MODULE__.Util

@type any_packet ::
OpenPGP.Packet.t()
| OpenPGP.PublicKeyEncryptedSessionKeyPacket.t()
| OpenPGP.SecretKeyPacket.t()
| OpenPGP.PublicKeyPacket.t()
| OpenPGP.CompressedDataPacket.t()
| OpenPGP.IntegrityProtectedDataPacket.t()
| OpenPGP.LiteralDataPacket.t()
%OpenPGP.Packet{}
| %OpenPGP.PublicKeyEncryptedSessionKeyPacket{}
| %OpenPGP.SecretKeyPacket{}
| %OpenPGP.PublicKeyPacket{}
| %OpenPGP.CompressedDataPacket{}
| %OpenPGP.IntegrityProtectedDataPacket{}
| %OpenPGP.LiteralDataPacket{}
| %OpenPGP.ModificationDetectionCodePacket{}

@doc """
Decode all packets in a message (input).
Expand Down Expand Up @@ -145,4 +148,18 @@ defmodule OpenPGP do
packet
end
end

@doc "Encode any packet (except for %Packet{}) that implements `OpenPGP.Encode` protocol."
@spec encode_packet(any_packet()) :: binary()
def encode_packet(%{} = packet) do
tag = Encode.tag(packet)
ptag = %PacketTag{format: :new, tag: tag}
body = Encode.encode(packet)

Encode.encode(%Packet{tag: ptag, body: body})
end

@doc "Encrypt any packet that implements `OpenPGP.Encrypt` protocol."
@spec encrypt_packet(packet, opts :: Keyword.t()) :: packet when packet: any_packet()
def encrypt_packet(%{} = packet, opts \\ []) when is_list(opts), do: Encrypt.encrypt(packet, opts)
end
15 changes: 15 additions & 0 deletions lib/open_pgp/encode.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defprotocol OpenPGP.Encode do
@spec tag(t) :: OpenPGP.Packet.PacketTag.tag_tuple()
def tag(packet)

@spec encode(t, opts :: Keyword.t()) :: binary()
def encode(packet, opts \\ [])
end

# There is a "bug" in dialyxir on Elixir 1.13/OTP24 and Elixir1.14/OTP25
# https://github.com/elixir-lang/elixir/issues/7708#issuecomment-403422965

defimpl OpenPGP.Encode, for: [Atom, BitString, Float, Function, Integer, List, Map, PID, Port, Reference, Tuple] do
def tag(subj), do: raise(Protocol.UndefinedError, protocol: @protocol, value: subj)
def encode(subj, _), do: raise(Protocol.UndefinedError, protocol: @protocol, value: subj)
end
18 changes: 18 additions & 0 deletions lib/open_pgp/encode/integrity_protected_data_packet_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defimpl OpenPGP.Encode, for: OpenPGP.IntegrityProtectedDataPacket do
alias OpenPGP.IntegrityProtectedDataPacket

def tag(_), do: {18, "Sym. Encrypted and Integrity Protected Data Packet"}

@doc """
Encode a Sym. Encrypted and Integrity Protected Data Packet.
Return encoded packet body.

### Example:

iex> packet = %OpenPGP.IntegrityProtectedDataPacket{ciphertext: "Ciphertext"}
...> OpenPGP.Encode.encode(packet)
<<1::8, "Ciphertext">>
"""
@version 1
def encode(%IntegrityProtectedDataPacket{ciphertext: ciphertext}, _opts), do: <<@version::8, ciphertext::binary>>
end
53 changes: 53 additions & 0 deletions lib/open_pgp/encode/literal_data_packet_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defimpl OpenPGP.Encode, for: OpenPGP.LiteralDataPacket do
alias OpenPGP.LiteralDataPacket

def tag(_), do: {11, "Literal Data Packet"}

@format_map %{
binary: <<0x62::8>>,
text: <<0x74::8>>,
text_utf8: <<0x75::8>>
}
@formats Map.keys(@format_map)

@doc """
Encode Literal Data Packet.
Return encoded packet body.

### Example:

iex> packet = %OpenPGP.LiteralDataPacket{
...> format: :binary,
...> file_name: "file.txt",
...> created_at: ~U[2022-02-24 02:30:00Z],
...> data: "Hello"
...> }
...> OpenPGP.Encode.encode(packet)
<<0x62, 8, "file.txt", 1645669800::32, "Hello">>

"""
def encode(%LiteralDataPacket{data: "" <> _ = data} = packet, _opts) do
format = packet.format || :binary

format_byte =
Map.get(@format_map, format) ||
raise """
Unknown Literal Data Packet format: #{inspect(packet.format)}.
Known formats: #{inspect(@formats)}
"""

fname_string =
case packet.file_name do
fname when is_binary(fname) and byte_size(fname) > 0 -> <<byte_size(fname)::8, fname::binary>>
nil -> <<0::8>>
end

timestamp =
case packet.created_at do
%DateTime{} = date -> DateTime.to_unix(date)
nil -> System.os_time(:second)
end

<<format_byte::binary, fname_string::binary, timestamp::32, data::binary>>
end
end
56 changes: 56 additions & 0 deletions lib/open_pgp/encode/packet/body_chunk_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defimpl OpenPGP.Encode, for: OpenPGP.Packet.BodyChunk do
alias OpenPGP.Packet.BodyChunk

def tag(_), do: raise(".tag/1 of protocol #{inspect(@protocol)} not supported by design for #{inspect(@for)}.")

@doc """
Encode body chunk. Always uses new packet format.
Return encoded body chunk with the length header prefix.

### Example:

Encodes one-octet length New Format Packet Length Header (up to 191 octets)

iex> OpenPGP.Encode.encode(%OpenPGP.Packet.BodyChunk{data: "Hello world!"})
<<12::8, "Hello world!">>

Encodes two-octet length New Format Packet Length Header (192-8383 octets)

iex> rand_bytes = :crypto.strong_rand_bytes(255)
...> OpenPGP.Encode.encode(%OpenPGP.Packet.BodyChunk{data: rand_bytes})
<<192::8, 63::8, rand_bytes::binary>>

Encodes five-octet length New Format Packet Length Header (8384-4_294_967_295 (0xFFFFFFFF) octets)

iex> rand_bytes = :crypto.strong_rand_bytes(8384)
...> OpenPGP.Encode.encode(%OpenPGP.Packet.BodyChunk{data: rand_bytes})
<<255::8, 8384::32, rand_bytes::binary>>
"""
@one_octet_length 0..191
@two_octet_length 192..8383
@five_octet_length 8384..0xFFFFFFFF
def encode(%BodyChunk{data: "" <> _ = data}, _opts) do
blen = byte_size(data)

hlen =
cond do
blen in @one_octet_length ->
<<blen::8>>

blen in @two_octet_length ->
<<b1::8, b2::8>> = <<blen - 192::16>>
<<b1 + 192::8, b2::8>>

blen in @five_octet_length ->
<<255::8, blen::32>>

true ->
raise """
Encoding of body chunks with length greater than 0xFFFFFFFF octets is not implemented.
Consider implementing a Partial Body Length Header.
"""
end

hlen <> data
end
end
23 changes: 23 additions & 0 deletions lib/open_pgp/encode/packet/packet_tag_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defimpl OpenPGP.Encode, for: OpenPGP.Packet.PacketTag do
alias OpenPGP.Packet.PacketTag

def tag(_), do: raise(".tag/1 of protocol #{inspect(@protocol)} not supported by design for #{inspect(@for)}.")

@doc """
Encode packet tag. Always uses new packet format.
Return encoded packet tag octet.

### Example:

iex> ptag = %OpenPGP.Packet.PacketTag{format: :new, tag: {1, "Public-Key Encrypted Session Key Packet"}}
...> OpenPGP.Encode.encode(ptag)
<<1::1, 1::1, 1::6>>

iex> ptag = %OpenPGP.Packet.PacketTag{format: :new, tag: {2, "Signature Packet"}}
...> OpenPGP.Encode.encode(ptag)
<<1::1, 1::1, 2::6>>
"""
def encode(%PacketTag{format: :new, tag: {tag_id, _desc}}, _opts) when is_integer(tag_id) and tag_id in 0..63 do
<<1::1, 1::1, tag_id::6>>
end
end
28 changes: 28 additions & 0 deletions lib/open_pgp/encode/packet_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defimpl OpenPGP.Encode, for: OpenPGP.Packet do
alias OpenPGP.Packet
alias OpenPGP.Packet.BodyChunk

def tag(_), do: raise(".tag/1 of protocol #{inspect(@protocol)} not supported by design for #{inspect(@for)}.")

@doc """
Encode a Packet.
Return encoded packet binary - a packet header, followed by the packet body.

### Example:

iex> ptag = %OpenPGP.Packet.PacketTag{format: :new, tag: {11, "Literal Data Packet"}}
...> OpenPGP.Encode.encode(%OpenPGP.Packet{tag: ptag, body: "Hello, World!!!"})
<<1::1, 1::1, 11::6, 15::8, "Hello, World!!!">>
"""
def encode(%Packet{} = packet, _opts) do
encoded_body =
case packet.body do
nil -> <<0::8>>
[] -> <<0::8>>
"" <> _ = body -> @protocol.encode(%BodyChunk{data: body}, [])
[%BodyChunk{} | _] = chunks -> Enum.reduce(chunks, "", fn chunk, acc -> acc <> @protocol.encode(chunk) end)
end

@protocol.encode(packet.tag) <> encoded_body
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defimpl OpenPGP.Encode, for: OpenPGP.PublicKeyEncryptedSessionKeyPacket do
alias OpenPGP.PublicKeyEncryptedSessionKeyPacket, as: PKESK

def tag(_), do: {1, "Public-Key Encrypted Session Key Packet"}

@doc """
Encode Public-Key Encrypted Session Key Packet.
Return encoded packet body.

### Example

iex> packet = %OpenPGP.PublicKeyEncryptedSessionKeyPacket{
...> ciphertext: "Ciphertext",
...> public_key_id: "6BAF2C48",
...> public_key_algo: {16, "Elgamal (Encrypt-Only) [ELGAMAL] [HAC]"}
...> }
...> OpenPGP.Encode.encode(packet)
<<3::8, "6BAF2C48", 16::8, "Ciphertext">>
"""
@version 3
def encode(%PKESK{ciphertext: ciphertext, public_key_id: key_id, public_key_algo: {key_algo_id, _}}, _opts)
when is_binary(ciphertext) and is_binary(key_id) and key_algo_id in 1..255 do
<<@version::8, key_id::binary, key_algo_id::8, ciphertext::binary>>
end
end
11 changes: 11 additions & 0 deletions lib/open_pgp/encrypt.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defprotocol OpenPGP.Encrypt do
@spec encrypt(t(), opts :: Keyword.t()) :: t()
def encrypt(packet, opts \\ [])
end

# There is a "bug" in dialyxir on Elixir 1.13/OTP24 and Elixir1.14/OTP25
# https://github.com/elixir-lang/elixir/issues/7708#issuecomment-403422965

defimpl OpenPGP.Encrypt, for: [Atom, BitString, Float, Function, Integer, List, Map, PID, Port, Reference, Tuple] do
def encrypt(subj, _), do: raise(Protocol.UndefinedError, protocol: @protocol, value: subj)
end
49 changes: 49 additions & 0 deletions lib/open_pgp/encrypt/integrity_protected_data_packet_impl.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defimpl OpenPGP.Encrypt, for: OpenPGP.IntegrityProtectedDataPacket do
alias OpenPGP.ModificationDetectionCodePacket, as: MDC
alias OpenPGP.IntegrityProtectedDataPacket, as: DataPacket
alias OpenPGP.Util

@doc """
Encrypt Sym. Encrypted Integrity Protected Data Packet with a given sym.algo and session key.

Require `OpenPGP.IntegrityProtectedDataPacket` fields:

- `:plaintext` - a binary

Accept options keyword list as a secont argument:

- `:session_key` - a key for sym.algo (required)
- `:session_key_algo` - a tuple representing sym.algo (required, see `t:Util.sym_algo_tuple()`)
- `:use_mdc` - the Modification Detection Code Packet added if set to `true` (optional, default `true`)

Return updated `OpenPGP.IntegrityProtectedDataPacket` with populated fields:

- `:ciphertext`
"""
def encrypt(%DataPacket{} = packet, opts) do
session_key =
Keyword.get(opts, :session_key) ||
raise """
Missing options key :session_key - a key for sym.algo
"""

session_key_algo =
Keyword.get(opts, :session_key_algo) ||
raise """
Missing options key :session_key_algo - a tuple representing sym.algo (see `t:Util.sym_algo_tuple()`)
"""

crypto_cipher = Util.sym_algo_to_crypto_cipher(session_key_algo)
null_iv = DataPacket.build_null_iv(session_key_algo)
checksum = DataPacket.build_checksum(session_key_algo)

data =
if Keyword.get(opts, :use_mdc, true),
do: MDC.append_to(checksum <> packet.plaintext),
else: checksum <> packet.plaintext

ciphertext = :crypto.crypto_one_time(crypto_cipher, session_key, null_iv, data, true)

%{packet | ciphertext: ciphertext}
end
end
Loading
Loading