Skip to content

Commit

Permalink
feat: decrypt json files
Browse files Browse the repository at this point in the history
  • Loading branch information
kkostov committed Jan 11, 2025
0 parents commit 2713354
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
env:
MIX_ENV: test
strategy:
fail-fast: false
matrix:
include:
- elixir: 1.18.1
otp: 27.0
lint: true
steps:
- uses: actions/checkout@v4

- uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
version-type: strict
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.PASSPHRASE }}

- run: gpg -K

- run: mix deps.get --only test

- run: mix format --check-formatted
if: ${{ matrix.lint }}

- run: mix deps.get && mix deps.unlock --check-unused
if: ${{ matrix.lint }}

- run: mix deps.compile

- run: mix compile --warnings-as-errors
if: ${{ matrix.lint }}

- run: mix test
35 changes: 35 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Publish

on:
workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-latest
name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
strategy:
matrix:
include:
- elixir: 1.17.1
otp: 27.0
lint: true
steps:
- uses: actions/checkout@v4

- uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
version-type: strict

- run: mix deps.get

- run: mix deps.compile

- run: mix compile --warnings-as-errors
if: ${{ matrix.lint }}

- name: Publish package
run: mix hex.publish --yes
env:
HEX_API_KEY: ${{ secrets.HEX_API_KEY }}
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
ex_sopsy-*.tar

# Temporary files, for example, from tests.
/tmp/
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# ExSopsy

Sopsy is a pragramtic wrapper around Mozila SOPS allowing decryption of secrets at runtime.

The goal of the library is to offer a simple solution for bringing encrypted secrets into your Elixir application.


## Requirements

* [Mozilla SOPS](https://github.com/getsops/sops) CLI must be installed on the system and available in the PATH.
* The Elixir application must have read access to the SOPS encrypted file.
* Use environment variables or `.sops.yaml` configuration file to configure the sops binary as needed.

## Usage

You can call `ExSopsy.load_secrets` passing a path to a SOPS encrypted file and the format of the file.
If decryption is successful, the function returns a tuple `{:ok, Map.t}` with the decrypted secret keys.

```elixir
# config/runtime.exs
if config_env() == :prod do
case ExSopsy.load_secrets("priv/secrets.enc.json", :json) do
{:ok, secrets} ->
config :my_app, MyApp.Repo,
username: secrets["db_user"],
password: secrets["db_password"]

config :my_app, MyAppWeb.Endpoint,
secret_key_base: secrets["secret_key_base"]

{:error, reason} ->
raise "Failed to load secrets: #{inspect(reason)}"
end
end

```


## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ex_sopsy` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:ex_sopsy, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ex_sopsy>.
25 changes: 25 additions & 0 deletions lib/ex_sopsy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule ExSopsy do
@moduledoc """
A library for interacting with Mozilla SOPS to fetch secrets at runtime.
"""

@doc """
Decrypts a SOPS-encrypted file and returns the secrets as a map.
## Examples
iex> ExSopsy.load_secrets("./test/files/doc.enc.json", :json)
{:ok, %{"example_key" => "example_value"}}
"""
def load_secrets(file_path, :json) do
case System.cmd("sops", ["-d", file_path], stderr_to_stdout: true) do
{result, 0} ->
case JSON.decode(result) do
{:ok, decoded} -> {:ok, decoded}
{:error, reason} -> {:error, {:invalid_json, reason}}
end

{error_output, _exit_code} ->
{:error, {:sops_failed, error_output}}
end
end
end
28 changes: 28 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule ExSopsy.MixProject do
use Mix.Project

def project do
[
app: :ex_sopsy,
version: "0.1.0",
elixir: "~> 1.18",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
14 changes: 14 additions & 0 deletions test/ex_sopsy_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule ExSopsyTest do
use ExUnit.Case
doctest ExSopsy

test "decrypts a json file" do
{:ok, result} = ExSopsy.load_secrets("./test/files/example.enc.json", :json)
IO.inspect(result, label: "result")
assert result["hello"] == "Handling Secrets should not be complicated"
assert result["example_number"] == 1234.56789
assert result["example_booleans"] == [true, false]
assert result["example_array"] == ["example_value1", "example_value2"]
assert result["example_key"] == "example_value"
end
end
21 changes: 21 additions & 0 deletions test/files/doc.enc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"example_key": "ENC[AES256_GCM,data:7FV0YARvjooQTyMnxQ==,iv:8X3+QEDH0pM6H3kbI2V2ZhofaYeAGtJWWB6pKUqdBdc=,tag:sEDiS07eekEMzl8qhoVFOA==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
"azure_kv": null,
"hc_vault": null,
"age": null,
"lastmodified": "2025-01-11T12:23:25Z",
"mac": "ENC[AES256_GCM,data:8SI7Tatx+w9OAlUeC3+gY04fAGB/qcB1Xmr9xb96cFmNuKxlDdODRCnAZbzs3mDCgh3vsrURpk7GHS9KslFCakNBohkcn/2FQmuCs4NwRYdbsCytOTtyw4G8vEhc+KdeuyWfPRZlmB0mPv8u9ZwJHIQrbFtAbTyvn4p4TrlFbXM=,iv:P2ziefFZlSJnFo79r6j2t2fWnzNyFmgVicUg3lWaFQY=,tag:9Vg4yJTd3RqmgOzaVkp8yA==,type:str]",
"pgp": [
{
"created_at": "2025-01-11T12:23:25Z",
"enc": "-----BEGIN PGP MESSAGE-----\n\nhQEMA4qRitmVgMc6AQgAghp4j5dYmixkJ5MBbMqjF1gtI+awUzS17Z/jsgPBbmIm\n3lCYfqU6HQeuMcLqyTAb3er4Pueh3vWa7ViUJYOtSRUY2LxDCwL0pFEn7SjhFCn6\nk1dY9+OMP/e7SneRvzIZRtCL3fQ/T46HCS6lXW1hkzSGTbCHpIEVj63HdclEMxhS\nnpgiFomceEoK0rJJnG22S6nFNuNtph+aKKAsCAZnw/lSv/YE7Dc00seeGFMiLWSD\n0dHvnS/IdvQ3+D1y6hcOgR0RPD8VtNYKO9f3MzbLJZPPoCjng8AWlsBAg9X6g9fx\nuHYXLDY4k37cq63CjNcRw4mv68GrZv656QZ66M23RdRoAQkCEEG0eqQd8rs2hCRK\nAgg2Wn5HkAHnlrfJW3c3ApuAgHX9XRIqh5Jla3Ky4CTATtVhoy8BwplvbFFMTyCT\nkfxQageLqMgu8JL2mWFEEn5HHG1czgU0TQ5EIBZRJxa9waURESq7vpU=\n=sX3R\n-----END PGP MESSAGE-----",
"fp": "8951791B8CE9D47DEB4AB9228A918AD99580C73A"
}
],
"unencrypted_suffix": "_unencrypted",
"version": "3.9.3"
}
}
31 changes: 31 additions & 0 deletions test/files/example.enc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"hello": "ENC[AES256_GCM,data:0Y+yObS/H6gDg/1bsxsX2H89+DsQPXLNtxbNYrYUxKcNRcNVQEmL+F92,iv:ov/Do/J1CfgjQxyhYg/S3pWEAVzwfbW6sbc4XU249SA=,tag:PurisCOF3JRDKu7o5xxO6A==,type:str]",
"example_key": "ENC[AES256_GCM,data:gn8CxrF7USkv78jsWQ==,iv:VnoopXp7e/kKDuvFAW12T3tApHZ3O9lnoZgT8yf0KS0=,tag:JL2Tabmytec0DIW18JNWRg==,type:str]",
"example_array": [
"ENC[AES256_GCM,data:ypT71B6lmQT9IWENrR0=,iv:xLC9bceQSzrfi31sdHUw1xehBqRTa8AVoAXP0A8YW6k=,tag:lZ0Dvx6cIY32ZzYZzNgApg==,type:str]",
"ENC[AES256_GCM,data:oOwbRehtShRD28flAVQ=,iv:l5uVi3Z5jUOVFzh2ECh8z89sc56Z5ZmmhPlIZOk2Ys8=,tag:Zg9+rAPQi/AMs00VLFhmIg==,type:str]"
],
"example_number": "ENC[AES256_GCM,data:O9F7kTHWAIZyjg==,iv:4ub1P4ZLZ0YbSsZpPJzlskUjSABsU/OvFZ0nhGGiv+g=,tag:IcBcnuKO8gXxMaD1aTjtYQ==,type:float]",
"example_booleans": [
"ENC[AES256_GCM,data:axYI/g==,iv:KdPg1+mkkFtvN4kapyCjgsCzgE6dOORNWnAZ3Yd+3hs=,tag:dHyj0YQH9lI9g3Q929ok5g==,type:bool]",
"ENC[AES256_GCM,data:BqPUcOE=,iv:RlEuR1YtiFWyixY/ByuPl6rY9mP5qNBXN0DObDpROcM=,tag:bwCPYA+4BpdV+f0Idr3rdg==,type:bool]"
],
"sops": {
"kms": null,
"gcp_kms": null,
"azure_kv": null,
"hc_vault": null,
"age": null,
"lastmodified": "2025-01-11T12:23:41Z",
"mac": "ENC[AES256_GCM,data:g2rkkycTEen7auUfQXzugyuQCIqhVE8WlYhlsjVSlbmAFQaSRog6q6ovZc3LtvmiWh2xAoCslFiKAMP/Fm6h287Vd5JnIV/wYcG8PaVt+0IiGS6n/+RQT3h1KyxzZ6pgbkXazW6fclfklpLnwOFkdi+cXAS9vuF7Sk2X0QlCaQg=,iv:kELlWnTvjzESu0F19xDhexv1HHl9K44xO3yJAaQMkwk=,tag:poyGqmCIfLz2rj4ll16+rA==,type:str]",
"pgp": [
{
"created_at": "2025-01-11T12:23:41Z",
"enc": "-----BEGIN PGP MESSAGE-----\n\nhQEMA4qRitmVgMc6AQgAiSErosiCjazfKtrjUnwyJjdHoKyMe4EfCD/ewZ9e/Nvb\nJiifeGOJSbf3FJI2r7ihz92bufTTurvi0lCX80NPCKWGqIxhoXClTp7nBN/bI+UV\nWVxh7dfq8462TDyBpN+Sb7MuNttQXEJiN0fBu7nIrrFbU4LIepSleBK8zFeaItVs\nxqf+DEE7QjDnAItrhKk6jD3B9JLvDGK1QJ7IzJEt7Z6GM3bE+Lt6b6B73CLC1UOA\nbsUpJ9/jMHxUh8Nl8+WFkwU6w0492MmrXGLNb2z2I6zVusXKmjEyerykZVSRjP+3\nI2dnDXx9+qDuQ9ggbg968oGnJYxy4OI4Nb+o1Fd4ZdRoAQkCEOKcu/rKWCS1p3Ou\nd02i8POpIE0na4IR/s47XtcsnZ+3hoYI2pHRZkiNeHWpoxR0Mc+C8jXXuiMbQDxA\nmnR47jdYINOGZrtRd8q9N8UealqxVk5P9xQ1EjtIg0OoJhILxIMfAnA=\n=Qag1\n-----END PGP MESSAGE-----",
"fp": "8951791B8CE9D47DEB4AB9228A918AD99580C73A"
}
],
"unencrypted_suffix": "_unencrypted",
"version": "3.9.3"
}
}
1 change: 1 addition & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()

0 comments on commit 2713354

Please sign in to comment.