Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Plug.Crypto #2252

Merged
merged 7 commits into from
Oct 6, 2023
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
6 changes: 3 additions & 3 deletions lib/livebook/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,12 @@ defmodule Livebook.Application do
"You specified LIVEBOOK_TEAMS_NAME, but LIVEBOOK_TEAMS_KEY is missing."
)

{secret_key, sign_secret} = Livebook.Teams.derive_keys(teams_key)
secret_key = Livebook.Teams.derive_key(teams_key)
id = "team-#{name}"

secrets =
if encrypted_secrets do
case Livebook.Teams.decrypt(encrypted_secrets, secret_key, sign_secret) do
case Livebook.Teams.decrypt(encrypted_secrets, secret_key) do
{:ok, json} ->
for {name, value} <- Jason.decode!(json),
do: %Livebook.Secrets.Secret{
Expand All @@ -283,7 +283,7 @@ defmodule Livebook.Application do

file_systems =
if encrypted_file_systems do
case Livebook.Teams.decrypt(encrypted_file_systems, secret_key, sign_secret) do
case Livebook.Teams.decrypt(encrypted_file_systems, secret_key) do
{:ok, json} ->
for %{"type" => type} = dumped_data <- Jason.decode!(json),
do: Livebook.FileSystems.load(type, dumped_data)
Expand Down
17 changes: 11 additions & 6 deletions lib/livebook/hubs/personal.ex
Original file line number Diff line number Diff line change
Expand Up @@ -237,17 +237,22 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Personal do
end

def notebook_stamp(personal, notebook_source, metadata) do
token = Livebook.Stamping.aead_encrypt(metadata, notebook_source, personal.secret_key)

stamp = %{"version" => 1, "token" => token}

token = Livebook.Stamping.chapoly_encrypt(metadata, notebook_source, personal.secret_key)
stamp = %{"version" => 2, "token" => token}
{:ok, stamp}
end

def verify_notebook_stamp(personal, notebook_source, stamp) do
%{"version" => 1, "token" => token} = stamp
case stamp do
%{"version" => 1, "token" => token} ->
Livebook.Stamping.aead_decrypt(token, notebook_source, personal.secret_key)

Livebook.Stamping.aead_decrypt(token, notebook_source, personal.secret_key)
%{"version" => 2, "token" => token} ->
Livebook.Stamping.chapoly_decrypt(token, notebook_source, personal.secret_key)

%{"version" => _} ->
{:error, :too_recent_version}
end
end

def dump(personal) do
Expand Down
2 changes: 1 addition & 1 deletion lib/livebook/hubs/provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ defprotocol Livebook.Hubs.Provider do
See `t:notebook_stamp/0` for more details.
"""
@spec verify_notebook_stamp(t(), iodata(), notebook_stamp()) ::
{:ok, metadata :: map()} | :error
{:ok, metadata :: map()} | {:error, :invalid | :too_recent_version}
def verify_notebook_stamp(hub, notebook_source, stamp)

@doc """
Expand Down
6 changes: 3 additions & 3 deletions lib/livebook/hubs/team.ex
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
# request to the Teams server (which ensures team membership).

@teams_key_prefix <> teams_key = team.teams_key
token = Livebook.Stamping.aead_encrypt(metadata, notebook_source, teams_key)
token = Livebook.Stamping.chapoly_encrypt(metadata, notebook_source, teams_key)

case Livebook.Teams.org_sign(team, token) do
{:ok, token_signature} ->
Expand All @@ -172,9 +172,9 @@ defimpl Livebook.Hubs.Provider, for: Livebook.Hubs.Team do
@public_key_prefix <> org_public_key = team.org_public_key

if Livebook.Stamping.rsa_verify?(token_signature, token, org_public_key) do
Livebook.Stamping.aead_decrypt(token, notebook_source, teams_key)
Livebook.Stamping.chapoly_decrypt(token, notebook_source, teams_key)
else
:error
{:error, :invalid}
end
end

Expand Down
16 changes: 7 additions & 9 deletions lib/livebook/hubs/team_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule Livebook.Hubs.TeamClient do
defstruct [
:hub,
:connection_error,
:derived_keys,
:derived_key,
connected?: false,
secrets: [],
file_systems: []
Expand Down Expand Up @@ -82,7 +82,7 @@ defmodule Livebook.Hubs.TeamClient do

@impl true
def init(%Hubs.Team{offline: nil} = team) do
derived_keys = Teams.derive_keys(team.teams_key)
derived_key = Teams.derive_key(team.teams_key)

headers = [
{"x-lb-version", to_string(Application.spec(:livebook, :vsn))},
Expand All @@ -93,18 +93,18 @@ defmodule Livebook.Hubs.TeamClient do
]

{:ok, _pid} = Teams.Connection.start_link(self(), headers)
{:ok, %__MODULE__{hub: team, derived_keys: derived_keys}}
{:ok, %__MODULE__{hub: team, derived_key: derived_key}}
end

def init(%Hubs.Team{} = team) do
derived_keys = Teams.derive_keys(team.teams_key)
derived_key = Teams.derive_key(team.teams_key)

{:ok,
%__MODULE__{
hub: team,
secrets: team.offline.secrets,
file_systems: team.offline.file_systems,
derived_keys: derived_keys
derived_key: derived_key
}}
end

Expand Down Expand Up @@ -165,8 +165,7 @@ defmodule Livebook.Hubs.TeamClient do
end

defp build_secret(state, %{name: name, value: value}) do
{secret_key, sign_secret} = state.derived_keys
{:ok, decrypted_value} = Teams.decrypt(value, secret_key, sign_secret)
{:ok, decrypted_value} = Teams.decrypt(value, state.derived_key)

%Secrets.Secret{
name: name,
Expand All @@ -189,8 +188,7 @@ defmodule Livebook.Hubs.TeamClient do
end

defp build_file_system(state, file_system) do
{secret_key, sign_secret} = state.derived_keys
{:ok, decrypted_value} = Teams.decrypt(file_system.value, secret_key, sign_secret)
{:ok, decrypted_value} = Teams.decrypt(file_system.value, state.derived_key)

dumped_data =
decrypted_value
Expand Down
16 changes: 11 additions & 5 deletions lib/livebook/live_markdown/import.ex
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ defmodule Livebook.LiveMarkdown.Import do
@invalid_stamp_message "invalid notebook stamp, disabling default access to secrets and remote files "
@personal_stamp_context "(you are either not the author of this notebook or changed its source outside of Livebook)"
@org_stamp_context "(this may happen if you made changes to the notebook source outside of Livebook)"
@too_recent_stamp_context "(the stamp has been generated using a more recent Livebook version, you need to upgrade)"

defp postprocess_stamp(notebook, _notebook_source, nil), do: {notebook, []}

Expand All @@ -616,12 +617,17 @@ defmodule Livebook.LiveMarkdown.Import do
notebook = apply_stamp_metadata(notebook, metadata)
{true, notebook, []}
else
_ ->
error ->
extra =
if notebook.hub_id == "personal-hub" do
@personal_stamp_context
else
@org_stamp_context
cond do
error == {:error, :too_recent_version} ->
@too_recent_stamp_context

notebook.hub_id == "personal-hub" ->
@personal_stamp_context

true ->
@org_stamp_context
end

{false, notebook, [@invalid_stamp_message <> extra]}
Expand Down
56 changes: 36 additions & 20 deletions lib/livebook/stamping.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,60 @@ defmodule Livebook.Stamping do
@doc """
Performs authenticated encryption with associated data (AEAD) [1].

Uses AES-GCM-128 [2]. Returns a single token which carries encrypted
`payload` and signature for both `payload` and `additional_data`.
Uses XChaCha20-Poly1305 [2]. Returns a single token which carries
encrypted `payload` and signature for both `payload` and
`additional_data`.

[1]: https://en.wikipedia.org/wiki/Authenticated_encryption#Authenticated_encryption_with_associated_data_(AEAD)
[2]: https://www.rfc-editor.org/rfc/rfc5116#section-5
[2]: https://en.wikipedia.org/wiki/XChaCha20-Poly1305
"""
@spec aead_encrypt(term(), String.t(), String.t()) :: String.t()
def aead_encrypt(payload, additional_data, secret_key) do
{secret, sign_secret} = derive_keys(secret_key)

@spec chapoly_encrypt(term(), String.t(), String.t()) :: String.t()
def chapoly_encrypt(payload, additional_data, secret_key) do
secret = derive_key(secret_key)
payload = :erlang.term_to_binary(payload)
Plug.Crypto.MessageEncryptor.encrypt(payload, additional_data, secret, sign_secret)
Plug.Crypto.MessageEncryptor.encrypt(payload, additional_data, secret, "unused")
end

@doc """
Decrypts and verifies data obtained from `chapoly_encrypt/3`.
"""
@spec chapoly_decrypt(String.t(), String.t(), String.t()) :: {:ok, term()} | {:error, :invalid}
def chapoly_decrypt(encrypted, additional_data, secret_key) do
secret = derive_key(secret_key)

case Plug.Crypto.MessageEncryptor.decrypt(encrypted, additional_data, secret, "unused") do
{:ok, payload} ->
{:ok, Plug.Crypto.non_executable_binary_to_term(payload)}

:error ->
{:error, :invalid}
end
end

@doc """
Decrypts and verifies data obtained from `aead_encrypt/3`.
Decrypts and verifies data obtained from AEAD using AES-GCM-128 [1].

Earlier Livebook versions implemented AEAD using AES-GCM-128 and
this function can be used to decrypt that data.

[1]: https://www.rfc-editor.org/rfc/rfc5116#section-5
"""
@spec aead_decrypt(String.t(), String.t(), String.t()) :: {:ok, term()} | :error
@spec aead_decrypt(String.t(), String.t(), String.t()) :: {:ok, term()} | {:error, :invalid}
def aead_decrypt(encrypted, additional_data, secret_key) do
{secret, sign_secret} = derive_keys(secret_key)
<<secret::16-bytes, sign_secret::16-bytes>> = derive_key(secret_key)

case Plug.Crypto.MessageEncryptor.decrypt(encrypted, additional_data, secret, sign_secret) do
{:ok, payload} ->
payload = Plug.Crypto.non_executable_binary_to_term(payload)
{:ok, payload}
{:ok, Plug.Crypto.non_executable_binary_to_term(payload)}

:error ->
:error
{:error, :invalid}
end
end

defp derive_keys(secret_key) do
defp derive_key(secret_key) do
binary_key = Base.url_decode64!(secret_key, padding: false)

<<secret::16-bytes, sign_secret::16-bytes>> =
Plug.Crypto.KeyGenerator.generate(binary_key, "notebook signing", cache: Plug.Crypto.Keys)

{secret, sign_secret}
Plug.Crypto.KeyGenerator.generate(binary_key, "notebook signing", cache: Plug.Crypto.Keys)
end

@doc """
Expand Down
26 changes: 11 additions & 15 deletions lib/livebook/teams.ex
Original file line number Diff line number Diff line change
Expand Up @@ -223,34 +223,30 @@ defmodule Livebook.Teams do
@doc """
Encrypts the given value with Teams key derived keys.
"""
@spec encrypt(String.t() | nil, bitstring(), bitstring()) :: String.t()
def encrypt(value, _secret, _sign_secret) when value in ["", nil], do: value
@spec encrypt(String.t() | nil, bitstring()) :: String.t()
def encrypt(value, _secret) when value in ["", nil], do: value

def encrypt(value, secret, sign_secret) do
Plug.Crypto.MessageEncryptor.encrypt(value, secret, sign_secret)
def encrypt(value, secret) do
Plug.Crypto.MessageEncryptor.encrypt(value, secret, "unused")
end

@doc """
Decrypts the given encrypted value with Teams key derived keys.
"""
@spec decrypt(String.t() | nil, bitstring(), bitstring()) :: {:ok, String.t()} | :error
def decrypt(value, _secret, _sign_secret) when value in ["", nil], do: value
@spec decrypt(String.t() | nil, bitstring()) :: {:ok, String.t()} | :error
def decrypt(value, _secret) when value in ["", nil], do: value

def decrypt(encrypted_value, secret, sign_secret) do
Plug.Crypto.MessageEncryptor.decrypt(encrypted_value, secret, sign_secret)
def decrypt(encrypted_value, secret) do
Plug.Crypto.MessageEncryptor.decrypt(encrypted_value, secret, "unused")
jonatanklosko marked this conversation as resolved.
Show resolved Hide resolved
end

@doc """
Derives the secret and sign secret from given `teams_key`.
"""
@spec derive_keys(String.t()) :: {bitstring(), bitstring()}
def derive_keys(@prefix <> teams_key) do
@spec derive_key(String.t()) :: bitstring()
def derive_key(@prefix <> teams_key) do
binary_key = Base.url_decode64!(teams_key, padding: false)

<<secret::16-bytes, sign_secret::16-bytes>> =
Plug.Crypto.KeyGenerator.generate(binary_key, "notebook secret", cache: Plug.Crypto.Keys)

{secret, sign_secret}
Plug.Crypto.KeyGenerator.generate(binary_key, "notebook secret", cache: Plug.Crypto.Keys)
end

defp add_org_errors(%Ecto.Changeset{} = changeset, errors_map) do
Expand Down
16 changes: 8 additions & 8 deletions lib/livebook/teams/requests.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ defmodule Livebook.Teams.Requests do
@spec create_secret(Team.t(), Secret.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
def create_secret(team, secret) do
{secret_key, sign_secret} = Teams.derive_keys(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key, sign_secret)
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)

headers = auth_headers(team)
params = %{name: secret.name, value: secret_value}
Expand All @@ -65,8 +65,8 @@ defmodule Livebook.Teams.Requests do
@spec update_secret(Team.t(), Secret.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
def update_secret(team, secret) do
{secret_key, sign_secret} = Teams.derive_keys(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key, sign_secret)
secret_key = Teams.derive_key(team.teams_key)
secret_value = Teams.encrypt(secret.value, secret_key)

headers = auth_headers(team)
params = %{name: secret.name, value: secret_value}
Expand All @@ -92,7 +92,7 @@ defmodule Livebook.Teams.Requests do
@spec create_file_system(Team.t(), FileSystem.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
def create_file_system(team, file_system) do
{secret_key, sign_secret} = Teams.derive_keys(team.teams_key)
secret_key = Teams.derive_key(team.teams_key)
headers = auth_headers(team)

type = FileSystems.type(file_system)
Expand All @@ -103,7 +103,7 @@ defmodule Livebook.Teams.Requests do
params = %{
name: name,
type: to_string(type),
value: Teams.encrypt(json, secret_key, sign_secret)
value: Teams.encrypt(json, secret_key)
}

post("/api/v1/org/file-systems", params, headers)
Expand All @@ -115,7 +115,7 @@ defmodule Livebook.Teams.Requests do
@spec update_file_system(Team.t(), FileSystem.t()) ::
{:ok, map()} | {:error, map() | String.t()} | {:transport_error, String.t()}
def update_file_system(team, file_system) do
{secret_key, sign_secret} = Teams.derive_keys(team.teams_key)
secret_key = Teams.derive_key(team.teams_key)
headers = auth_headers(team)

type = FileSystems.type(file_system)
Expand All @@ -127,7 +127,7 @@ defmodule Livebook.Teams.Requests do
id: file_system.external_id,
name: name,
type: to_string(type),
value: Teams.encrypt(json, secret_key, sign_secret)
value: Teams.encrypt(json, secret_key)
}

put("/api/v1/org/file-systems", params, headers)
Expand Down
4 changes: 2 additions & 2 deletions lib/livebook_web/live/hub/edit/team_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,11 @@ defmodule LivebookWeb.Hub.Edit.TeamComponent do
end

defp encrypt_to_dockerfile(socket, data) do
{secret_key, sign_secret} = Livebook.Teams.derive_keys(socket.assigns.hub.teams_key)
secret_key = Livebook.Teams.derive_key(socket.assigns.hub.teams_key)

data
|> Jason.encode!()
|> Livebook.Teams.encrypt(secret_key, sign_secret)
|> Livebook.Teams.encrypt(secret_key)
end

@zta_options for provider <- Livebook.Config.identity_providers(),
Expand Down
6 changes: 3 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@ defmodule Livebook.MixProject do
#
defp deps do
[
{:phoenix, "~> 1.7.7"},
{:phoenix_html, "~> 3.0"},
# {:phoenix_live_view, "~> 0.20.0"},
{:phoenix, github: "phoenixframework/phoenix", override: true},
{:phoenix_live_view, github: "phoenixframework/phoenix_live_view", override: true},
{:phoenix_html, "~> 3.0"},
{:phoenix_live_dashboard, "~> 0.8.0"},
{:telemetry_metrics, "~> 0.4"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:plug_crypto, "~> 2.0"},
{:earmark_parser, "~> 1.4"},
{:castore, "~> 1.0"},
{:ecto, "~> 3.10"},
Expand Down
Loading
Loading