Skip to content

Commit

Permalink
feature: Encrypt files in object storage kitsteam#255
Browse files Browse the repository at this point in the history
  • Loading branch information
nwittstruck committed Apr 24, 2024
1 parent 96a1c22 commit c662b42
Show file tree
Hide file tree
Showing 16 changed files with 72 additions and 129 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/on_push_branch__execute_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
# Containers must run in Linux based operating systems
runs-on: ubuntu-latest
# Docker Hub image that `container-job` executes in
container: hexpm/elixir:1.15.2-erlang-26.0.2-debian-bullseye-20230612-slim
container: hexpm/elixir:1.15.7-erlang-26.2.2-debian-bullseye-20240130-slim

needs: build_deps

Expand Down Expand Up @@ -68,6 +68,7 @@ jobs:
DATABASE_SSL: false
# This should only be used for the tests:
SECRET_KEY_BASE: "DFvi5ZWW/Xc/yBgPOQ0w1wWZEGjy7NMl1/fRFyWf2EgbWNcXqtAOKUjh4bVps/eQ"
VAULT_ENCRYPTION_KEY_BASE64: "h3NKY+sEnQYSSoM7Qm0mNzHgwT99GD+TQVdz+q0sdEA="
MIX_ENV: "test"
OBJECT_STORAGE_BUCKET: "qrstorage-test"

Expand All @@ -93,7 +94,7 @@ jobs:

check_mix_format:
runs-on: ubuntu-latest
container: hexpm/elixir:1.15.2-erlang-26.0.2-debian-bullseye-20230612-slim
container: hexpm/elixir:1.15.7-erlang-26.2.2-debian-bullseye-20240130-slim

needs: build_deps

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Watch out that this content security policy will block live reload in developmen

### Image upload

It is possible to store images base 64 encoded in the database. Since this is not very efficient, this should not be used for large imsage sizes or for qr codes with a lot of traffic. The default limit for a text qr code is 2MB. To change this, change `QR_CODE_MAX_UPLOAD_LENGTH`. We automatically add a buffer to account for deltas as well as overhead.
It is possible to store images base 64 encoded in the database. Since this is not very efficient, this should not be used for large image sizes or for qr codes with a lot of traffic. The default limit for a text qr code is 2MB. To change this, change `QR_CODE_MAX_UPLOAD_LENGTH`. We automatically add a buffer to account for deltas as well as overhead.

### Additonal licence

Expand Down
26 changes: 0 additions & 26 deletions README_KITS.md

This file was deleted.

8 changes: 8 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ if config_env() == :prod || config_env() == :dev do
)
end

# configure cloak:
config :qrstorage, Qrstorage.Services.Vault,
ciphers: [
default:
{Cloak.Ciphers.AES.GCM,
tag: "AES.GCM.V1", key: Base.decode64!(System.fetch_env!("VAULT_ENCRYPTION_KEY_BASE64")), iv_length: 12}
]

# from mix phx.gen.release
if System.get_env("PHX_SERVER") do
config :qrstorage, QrstorageWeb.Endpoint, server: true
Expand Down
61 changes: 0 additions & 61 deletions docker-compose-kits.yml

This file was deleted.

2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ services:
OBJECT_STORAGE_HOST: ${MINIO_HOST:-minio}
OBJECT_STORAGE_BUCKET: qrstorage-dev
QR_CODE_DEFAULT_LOCALE: "de"
# Replace this in production with your own key!
VAULT_ENCRYPTION_KEY_BASE64: "t+OOnuWntk0vLcMqnF8nyKp+EuKAK+FnUU8OpdN9RoA="
# GCP_CONFIG_PATH: ".gcp-config.json"
# Please create a new secret for production runs:
# - Use `mix phx.gen.secret` if you have elixir and phoenix installed
Expand Down
1 change: 1 addition & 0 deletions lib/qrstorage/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule Qrstorage.Application do
{Phoenix.PubSub, name: Qrstorage.PubSub},
# Start the Endpoint (http/https)
QrstorageWeb.Endpoint,
Qrstorage.Services.Vault,
{Oban, oban_config()}
]

Expand Down
8 changes: 4 additions & 4 deletions lib/qrstorage/services/qr_code_service.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ defmodule Qrstorage.Services.QrCodeService do
{:ok} ->
{:ok, qr_code}

{:error, _error_message} ->
Logger.error("Audio file not stored: #{qr_code.id}")
{:error, error_message} ->
Logger.error("Audio file not stored: #{qr_code.id} Error: #{error_message}")
{:error, gettext("Qr code recording not extracted")}
end

Expand All @@ -92,8 +92,8 @@ defmodule Qrstorage.Services.QrCodeService do
{:ok} ->
{:ok, qr_code}

{:error, _error_message} ->
Logger.error("Audio file not stored: #{qr_code.id}")
{:error, error_message} ->
Logger.error("Audio file not stored: #{qr_code.id} Error: #{error_message}")
{:error, gettext("Qr code tts not stored")}
end

Expand Down
15 changes: 13 additions & 2 deletions lib/qrstorage/services/storage_service.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule Qrstorage.Services.StorageService do
alias Qrstorage.Services.ObjectStorage.ObjectStorageService
alias Qrstorage.Services.Vault
require Logger

def get_file_by_type(id, :recording) do
Expand Down Expand Up @@ -47,7 +48,10 @@ defmodule Qrstorage.Services.StorageService do
{:ok, response} ->
case response.status_code do
200 ->
{:ok, response.body}
case Vault.decrypt(response.body) do
{:ok, decrypted_file} -> {:ok, decrypted_file}
{:error, error_message} -> {:error, "Issue while decrypting file: #{inspect(error_message)}"}
end

_ ->
Logger.error(
Expand All @@ -67,7 +71,14 @@ defmodule Qrstorage.Services.StorageService do
end

defp store_file(filename, file, qr_code_content_type, content_type) do
case ObjectStorageService.put_object(bucket_name(), bucket_path(filename, qr_code_content_type), file, %{
case Vault.encrypt(file) do
{:ok, encrypted_file} -> store_encrypted_file(filename, encrypted_file, qr_code_content_type, content_type)
{:error, error_message} -> {:error, "Issue while encrypting file: #{inspect(error_message)}"}
end
end

defp store_encrypted_file(filename, encrypted_file, qr_code_content_type, content_type) do
case ObjectStorageService.put_object(bucket_name(), bucket_path(filename, qr_code_content_type), encrypted_file, %{
meta: %{"qr-code-content-type" => qr_code_content_type},
content_type: content_type
}) do
Expand Down
3 changes: 3 additions & 0 deletions lib/qrstorage/services/vault.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
defmodule Qrstorage.Services.Vault do
use Cloak.Vault, otp_app: :qrstorage
end
4 changes: 3 additions & 1 deletion lib/qrstorage_web/controllers/qr_code_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,10 @@ defmodule QrstorageWeb.QrCodeController do
|> send_resp(200, audio_file)

{:error, error_message} ->
Logger.error(error_message)

conn
|> send_resp(404, error_message)
|> send_resp(404, "Error while retrieving file")
end
end
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ defmodule Qrstorage.MixProject do
{:mox, "1.1.0", only: :test},
{:logger_json, "5.1.4"},
{:ex_aws, "2.5.3"},
{:ex_aws_s3, "2.5.3"}
{:ex_aws_s3, "2.5.3"},
{:cloak, "1.1.4"}
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
%{
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
Expand Down
11 changes: 3 additions & 8 deletions test/qrstorage/services/storage_service_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule Qrstorage.Services.StorageServiceTest do
use Qrstorage.DataCase
use Qrstorage.StorageCase

alias Qrstorage.Services.StorageService

Expand All @@ -11,10 +12,7 @@ defmodule Qrstorage.Services.StorageServiceTest do
test "get_recording/1 with a 200 result from the object storage service returns :ok" do
mock_file_content = "binary file content"

Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:get_object, fn _bucket_name, _bucket_path ->
{:ok, %{status_code: 200, body: mock_file_content}}
end)
mockStorageServiceGetFileSuccess(mock_file_content)

{status, file_content} = StorageService.get_recording("1")
assert status == :ok
Expand Down Expand Up @@ -151,10 +149,7 @@ defmodule Qrstorage.Services.StorageServiceTest do
test "get_tts/1 with a 200 result from the object storage service returns :ok" do
mock_file_content = "binary file content"

Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:get_object, fn _bucket_name, _bucket_path ->
{:ok, %{status_code: 200, body: mock_file_content}}
end)
mockStorageServiceGetFileSuccess(mock_file_content)

{status, file_content} = StorageService.get_tts("1")
assert status == :ok
Expand Down
31 changes: 10 additions & 21 deletions test/qrstorage_web/controllers/qr_code_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule QrstorageWeb.QrCodeControllerTest do
use QrstorageWeb.ConnCase
use Qrstorage.StorageCase

alias Qrstorage.QrCodes
alias Qrstorage.QrCodes.QrCode
Expand Down Expand Up @@ -120,11 +121,7 @@ defmodule QrstorageWeb.QrCodeControllerTest do
{:ok, translated_file_content}
end)

Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:put_object, fn _bucket_name, _bucket_path, file, _opts ->
assert file == audio_binary
{:ok, %{status_code: 200, body: audio_binary}}
end)
mockStorageServicePutObjectSuccess(audio_binary)

audio_attrs = %{
@create_attrs
Expand Down Expand Up @@ -152,17 +149,13 @@ defmodule QrstorageWeb.QrCodeControllerTest do
Qrstorage.Services.Gcp.GoogleApiServiceMock
|> expect(:text_to_audio, fn text, _language, _voice ->
assert text == translated_text
{:ok, "audio binary"}
{:ok, audio_binary}
end)
|> expect(:translate, fn _text, _language ->
{:ok, translated_text}
end)

Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:put_object, fn _bucket_name, _bucket_path, file, _opts ->
assert file == audio_binary
{:ok, %{status_code: 200, body: audio_binary}}
end)
mockStorageServicePutObjectSuccess(audio_binary)

audio_attrs = %{
@create_attrs
Expand Down Expand Up @@ -362,14 +355,12 @@ defmodule QrstorageWeb.QrCodeControllerTest do
setup [:create_audio_qr_code]

test "that audio codes can be downloaded when a file is present", %{conn: conn, audio_qr_code: audio_qr_code} do
Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:get_object, fn _bucket_name, _bucket_path ->
{:ok, %{status_code: 200, body: "file content"}}
end)
file = "file content"
mockStorageServiceGetFileSuccess(file)

conn = get(conn, Routes.qr_code_path(conn, :audio_file, audio_qr_code.id))

assert conn.resp_body == "file content"
assert conn.resp_body == file
assert conn.resp_headers |> get_content_type == "audio/mp3"
end

Expand Down Expand Up @@ -410,14 +401,12 @@ defmodule QrstorageWeb.QrCodeControllerTest do
conn: conn,
recording_qr_code: recording_qr_code
} do
Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:get_object, fn _bucket_name, _bucket_path ->
{:ok, %{status_code: 200, body: "file content"}}
end)
file = "file content"
mockStorageServiceGetFileSuccess(file)

conn = get(conn, Routes.qr_code_path(conn, :audio_file, recording_qr_code.id))

assert conn.resp_body == "file content"
assert conn.resp_body == file
assert conn.resp_headers |> get_content_type == "audio/mp3"
end
end
Expand Down
20 changes: 18 additions & 2 deletions test/support/storage_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Qrstorage.StorageCase do
"""
import Mox
use ExUnit.CaseTemplate
alias Qrstorage.Services.Vault

using do
quote do
Expand All @@ -14,10 +15,14 @@ defmodule Qrstorage.StorageCase do
end
end

def mockStorageServicePutObjectSuccess(mock_file_content \\ "mock http body") do
def mockStorageServicePutObjectSuccess(mock_file_content \\ "mock http body", assert_file_content \\ nil) do
Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:put_object, fn _bucket_name, _bucket_path, _file, _opts ->
{:ok, %{status_code: 200, body: mock_file_content}}
if assert_file_content != nil do
assert mock_file_content == assert_file_content
end

{:ok, %{status_code: 200, body: Vault.encrypt!(mock_file_content)}}
end)
end

Expand All @@ -39,6 +44,17 @@ defmodule Qrstorage.StorageCase do
end)
end

def mockStorageServiceGetFileSuccess(file, assert_file_content \\ nil) do
Qrstorage.Services.ObjectStorage.ObjectStorageServiceMock
|> expect(:get_object, fn _bucket_name, _bucket_path ->
if assert_file_content do
assert file == assert_file_content
end

{:ok, %{status_code: 200, body: Vault.encrypt!(file)}}
end)
end

def writeTmpFile(tmp_dir, filename \\ "recording.mp3", content \\ "binary audio content") do
file_path = Path.join(tmp_dir, filename)
File.write(file_path, content)
Expand Down

0 comments on commit c662b42

Please sign in to comment.