Skip to content

Commit

Permalink
Added CI tests (#1)
Browse files Browse the repository at this point in the history
* Added CI tests

* Readme updated
  • Loading branch information
paveltyk authored Mar 19, 2024
1 parent afe6d61 commit 02b800b
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 11 deletions.
66 changes: 66 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

permissions:
pull-requests: write

jobs:
unit_tests:
name: Unit tests Elixir ${{matrix.elixir}} / OTP ${{matrix.otp}}
runs-on: ubuntu-20.04

strategy:
matrix:
include:
- elixir: "1.16"
otp: "26"
- elixir: "1.15"
otp: "26"
- elixir: "1.14"
otp: "25"
- elixir: "1.13"
otp: "24"

steps:
- name: Checkout
uses: actions/checkout@v3

- name: Set up Elixir
uses: erlef/setup-elixir@v1
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}

- name: Restore deps cache
uses: actions/cache@v3
with:
path: deps
key: ${{ runner.os }}-mix-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}

- name: Restore _build cache
uses: actions/cache@v3
with:
path: _build
key: ${{ runner.os }}-build-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}

- name: Install deps
run: mix deps.get

- name: Check Formatting
run: mix format --check-formatted

- name: Run unit tests
run: |
mix clean
mix test
- name: Run dialyzer
run: |
MIX_ENV=test mix dialyzer
134 changes: 124 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# OpenPGP

[![Build Status](https://github.com/DivvyPayHQ/open_pgp/workflows/CI/badge.svg)](https://github.com/DivvyPayHQ/open_pgp/actions?query=workflow%3ACI)
[![Hex pm](https://img.shields.io/hexpm/v/open_pgp.svg)](https://hex.pm/packages/open_pgp)
[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/open_pgp/)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

OpenPGP lib allows to inspect, decode and decrypt OpenPGP Message Format as per [RFC4880](https://www.ietf.org/rfc/rfc4880.html)

## Installation
Expand All @@ -18,7 +23,7 @@ end

The `OpenPGP.Packet` is a generic packet type. It has an essential purpose: split OpenPGP message in packets and decode packet tags.

An OpenPGP message is constructed from a number of records that are traditionally called packets. A packet is a chunk of data that has a tag specifying its meaning. An OpenPGP message, keyring, certificate, and so forth consists of a number of packets. Some of those packets may contain other OpenPGP packets (for example, a compressed data packet, when uncompressed, contains OpenPGP packets). Each packet consists of a packet header, followed by the packet body. For more details refer to [Packet Syntax chapter in RFC4880](https://www.ietf.org/rfc/rfc4880.html#section-4)
An OpenPGP message is constructed from a number of records that are traditionally called packets. A packet is a chunk of data that has a tag specifying its meaning. An OpenPGP message, keyring, certificate, and so forth consists of a number of packets. Some of those packets may contain other OpenPGP packets (for example, a compressed data packet, when uncompressed, contains OpenPGP packets). Each packet consists of a packet header, followed by the packet body. For more details refer to [Packet Syntax chapter in RFC4880](https://www.ietf.org/rfc/rfc4880.html#section-4)

Once OpenPGP message split into generic packets, the higher order tag-specific packet decoders can be applied on its' data. Example:

Expand Down Expand Up @@ -69,7 +74,7 @@ iex> OpenPGP.cast_packets(packets)

### Decode Generic OpenPGP packet

In this example the packet tag specifies a Signature Packet with body length of 7 bytes. The remaining binary will be return as a second element in a two element tuple. More details in `OpenPGP.Packet.Behaviour`.
In this example the packet tag specifies a Signature Packet with body length of 7 bytes. The remaining binary will be return as a second element in a two element tuple. More details in `OpenPGP.Packet.Behaviour`.

```
iex> alias OpenPGP.Packet
Expand All @@ -85,6 +90,91 @@ iex> Packet.decode(<<1::1, 0::1, 2::4, 0::2, 7::8, "Hello, World!!!">>)
}
```

### Load private key and decrypt PGP file

This example assumes that the private key and encrypted message were exported in a raw binary format, which might not be the most common way of exporting PGP entries. If "armored" format ([Radix64](https://www.rfc-editor.org/rfc/rfc4880.html#section-6)) is used for exporting data (i.e., `gpg --armor --export ...`), you'll need to use `OpenPGP.Radix64.decode/1` first on file contents to get a list of entries and operate on its' `:data` attribute.

```
alias OpenPGP.Packet
alias OpenPGP.Packet.PacketTag
alias OpenPGP.CompressedDataPacket
alias OpenPGP.IntegrityProtectedDataPacket
alias OpenPGP.LiteralDataPacket
alias OpenPGP.PublicKeyPacket
alias OpenPGP.PublicKeyEncryptedSessionKeyPacket
alias OpenPGP.SecretKeyPacket
###################################
### Load encrypted message/file ###
###################################
encrypted_file = File.read!("test/fixtures/words.dict.gpg")
[
%PublicKeyEncryptedSessionKeyPacket{} = pkesk_packet,
%IntegrityProtectedDataPacket{} = ipdata_packet
] = encrypted_file |> OpenPGP.list_packets() |> OpenPGP.cast_packets()
%PublicKeyEncryptedSessionKeyPacket{public_key_id: public_key_id} = pkesk_packet
#######################
### Load secret key ###
#######################
private_key_file = File.read!("test/fixtures/rsa2048-priv.pgp")
passphrase = "passphrase"
keyring =
[
%SecretKeyPacket{},
%Packet{tag: %PacketTag{tag: {13, "User ID Packet"}}},
%Packet{tag: %PacketTag{tag: {2, "Signature Packet"}}},
%SecretKeyPacket{},
%Packet{tag: %PacketTag{tag: {2, "Signature Packet"}}}
] = private_key_file |> OpenPGP.list_packets() |> OpenPGP.cast_packets()
sk_packet =
Enum.find_value(keyring, fn
%SecretKeyPacket{public_key: %PublicKeyPacket{id: ^public_key_id}} = packet -> packet
_ -> nil
end)
sk_packet_decrypted = SecretKeyPacket.decrypt(sk_packet, passphrase)
################################
### Decode encrypted message ###
################################
pkesk_packet_decrypted = PublicKeyEncryptedSessionKeyPacket.decrypt(pkesk_packet, sk_packet_decrypted)
ipdata_packet_decrypted = IntegrityProtectedDataPacket.decrypt(ipdata_packet, pkesk_packet_decrypted)
%IntegrityProtectedDataPacket{
version: 1,
ciphertext: "" <> _,
plaintext: plaintext
} = ipdata_packet_decrypted
[
%CompressedDataPacket{
algo: {2, "ZLIB [RFC1950]"},
data_deflated: <<_::bitstring>>,
data_inflated: data_inflated
}
] = plaintext |> OpenPGP.list_packets() |> OpenPGP.cast_packets()
[
%LiteralDataPacket{
format: {<<0x62>>, :binary},
file_name: "words.dict",
created_at: ~U[2024-01-04 00:27:32Z],
data: data
}
] = data_inflated |> OpenPGP.list_packets() |> OpenPGP.cast_packets()
IO.puts(data)
```

### CompressedDataPacket

The `OpenPGP.CompressedDataPacket` will inflate data implicitly when decoded (also, data inflated implicitly when `OpenPGP.cast_packets/1` used).
Expand Down Expand Up @@ -150,20 +240,44 @@ iex> IntegrityProtectedDataPacket.decrypt(packet_decoded, pkesk)
}
```

### Radix64 / Armored format

```
payload = """
-----BEGIN PGP MESSAGE-----
Version: OpenPrivacy 0.99
yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS
vBSFjNSiVHsuAA==
=njUN
-----END PGP MESSAGE-----
"""
[
%Radix64.Entry{
crc: <<158, 53, 13>>,
data: <<200, 56, 1, 59, _::binary>> = data,
meta: [{"Version", "OpenPrivacy 0.99"}],
name: "PGP MESSAGE"
}
] = OpenPGP.Radix64.decode(payload)
"""
```

## Notes

As of **v0.5.x**:

1. Any valid OpenPGP message can be decoded via generic `OpenPGP.Packet` decoder. This abstraction layer provide Packet Tags and Body Chunks for packet envelope level evaluation.
1. Some Packet Tag specific decoders implemented with limited feature support:
1. `OpenPGP.LiteralDataPacket`
1. `OpenPGP.PublicKeyEncryptedSessionKeyPacket`
1. `OpenPGP.PublicKeyPacket` - support only V4 packets
1. `OpenPGP.SecretKeyPacket` - support only V4 packets; Iterated and Salted String-to-Key (S2K) specifier (ID: 3); S2K usage convention octet of 254 only; S2K hashing algo SHA1; AES128 symmetric encryption of secret key material
1. `OpenPGP.CompressedDataPacket` - support only ZLIB- and ZIP-style blocks
1. `OpenPGP.IntegrityProtectedDataPacket` - support Session Key algo 9 (AES with 256-bit key) in CFB mode; Modification Detection Code system is not supported

At a high level `OpenPGP.list_packets/1` and `OpenPGP.cast_packets/1` serve as an entrypoint to OpenPGP Message decoding and extracting generic data.
1. `OpenPGP.LiteralDataPacket`
1. `OpenPGP.PublicKeyEncryptedSessionKeyPacket`
1. `OpenPGP.PublicKeyPacket` - support only V4 packets
1. `OpenPGP.SecretKeyPacket` - support only V4 packets; Iterated and Salted String-to-Key (S2K) specifier (ID: 3); S2K usage convention octet of 254 only; S2K hashing algo SHA1; AES128 symmetric encryption of secret key material
1. `OpenPGP.CompressedDataPacket` - support only ZLIB- and ZIP-style blocks
1. `OpenPGP.IntegrityProtectedDataPacket` - support Session Key algo 9 (AES with 256-bit key) in CFB mode; Modification Detection Code system is not supported

At a high level `OpenPGP.list_packets/1` and `OpenPGP.cast_packets/1` serve as an entrypoint to OpenPGP Message decoding and extracting generic data.

Packet specific decoders implement `OpenPGP.Packet.Behaviour`, which exposes `.decode/1` interface (including genric `OpenPGP.Packet`). Additionaly some of the packet specific decoders may provide interface for further packet processing, such as `OpenPGP.SecretKeyPacket.decrypt/2`.

Expand Down
2 changes: 1 addition & 1 deletion lib/open_pgp/secret_key_packet.ex
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ defmodule OpenPGP.SecretKeyPacket do
<<s2k_usage::8, sym_algo::8, next::binary>> when s2k_usage == 254 ->
{s2k_specifier, next} = S2KSpecifier.decode(next)
iv_size = Util.sym_algo_cipher_block_size(sym_algo)
<<iv::bits-size(iv_size), ciphertext::binary()>> = next
<<iv::bits-size(iv_size), ciphertext::binary>> = next

packet = %__MODULE__{
public_key: public_key,
Expand Down

0 comments on commit 02b800b

Please sign in to comment.