From 02b800b6df509ae10170121d0a2e1727ee464363 Mon Sep 17 00:00:00 2001 From: Pavel Tsiukhtsiayeu Date: Tue, 19 Mar 2024 16:55:04 -0400 Subject: [PATCH] Added CI tests (#1) * Added CI tests * Readme updated --- .github/workflows/ci.yml | 66 +++++++++++++++ README.md | 134 +++++++++++++++++++++++++++--- lib/open_pgp/secret_key_packet.ex | 2 +- 3 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..91ed19e --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index ea22d9c..1463a11 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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 @@ -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). @@ -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`. diff --git a/lib/open_pgp/secret_key_packet.ex b/lib/open_pgp/secret_key_packet.ex index efc13c3..4b88c45 100644 --- a/lib/open_pgp/secret_key_packet.ex +++ b/lib/open_pgp/secret_key_packet.ex @@ -149,7 +149,7 @@ defmodule OpenPGP.SecretKeyPacket do <> when s2k_usage == 254 -> {s2k_specifier, next} = S2KSpecifier.decode(next) iv_size = Util.sym_algo_cipher_block_size(sym_algo) - <> = next + <> = next packet = %__MODULE__{ public_key: public_key,