Skip to content

Commit

Permalink
Add multiple secrets support (#58)
Browse files Browse the repository at this point in the history
Now transaction data keys contains:
- secrets: list of encrypted data
- authorized keys: list of authorized keys by secret

So we can have some secrets shared by the nodes and some secrets for
the owners of the transaction chain
  • Loading branch information
Samuel authored Sep 3, 2021
1 parent 2c0455f commit a4b409b
Show file tree
Hide file tree
Showing 38 changed files with 622 additions and 510 deletions.
6 changes: 3 additions & 3 deletions lib/archethic/contracts/contract/conditions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule ArchEthic.Contracts.Contract.Conditions do
:content,
:code,
:authorized_keys,
:secret,
:secrets,
:uco_transfers,
:nft_transfers,
:previous_public_key,
Expand All @@ -23,7 +23,7 @@ defmodule ArchEthic.Contracts.Contract.Conditions do
content: binary() | Macro.t() | nil,
code: binary() | Macro.t() | nil,
authorized_keys: map() | Macro.t() | nil,
secret: binary() | Macro.t() | nil,
secrets: list(binary()) | Macro.t() | nil,
uco_transfers: map() | Macro.t() | nil,
nft_transfers: map() | Macro.t() | nil,
previous_public_key: binary() | Macro.t() | nil,
Expand All @@ -35,7 +35,7 @@ defmodule ArchEthic.Contracts.Contract.Conditions do
content: nil,
code: nil,
authorized_keys: nil,
secret: nil,
secrets: nil,
uco_transfers: nil,
nft_transfers: nil,
previous_public_key: nil
Expand Down
17 changes: 9 additions & 8 deletions lib/archethic/contracts/contract/constants.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ defmodule ArchEthic.Contracts.Contract.Constants do
data: %TransactionData{
content: content,
code: code,
keys: %Keys{
authorized_keys: authorized_keys,
secret: secret
},
keys:
keys = %Keys{
authorized_keys: authorized_keys,
secrets: secrets
},
ledger: %Ledger{
uco: %UCOLedger{
transfers: uco_transfers
Expand All @@ -50,9 +51,9 @@ defmodule ArchEthic.Contracts.Contract.Constants do
"type" => Atom.to_string(type),
"content" => content,
"code" => code,
"authorized_public_keys" => Map.keys(authorized_keys),
"authorized_public_keys" => Keys.list_authorized_public_keys(keys),
"authorized_keys" => authorized_keys,
"secret" => secret,
"secrets" => secrets,
"previous_public_key" => previous_public_key,
"recipients" => recipients,
"uco_transfers" =>
Expand Down Expand Up @@ -84,8 +85,8 @@ defmodule ArchEthic.Contracts.Contract.Constants do
code: Map.get(constants, "code", ""),
content: Map.get(constants, "content", ""),
keys: %Keys{
authorized_keys: Map.get(constants, "authorized_keys", %{}),
secret: Map.get(constants, "secret", "")
authorized_keys: Map.get(constants, "authorized_keys", []),
secrets: Map.get(constants, "secrets", [])
},
recipients: Map.get(constants, "recipients", []),
ledger: %Ledger{
Expand Down
48 changes: 21 additions & 27 deletions lib/archethic/contracts/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ defmodule ArchEthic.Contracts.Interpreter do
"uco_transfers",
"nft_transfers",
"authorized_public_keys",
"secret",
"secrets",
"recipients"
]

Expand Down Expand Up @@ -238,6 +238,9 @@ defmodule ArchEthic.Contracts.Interpreter do
{{:error, :unexpected_token}, {{:atom, key}, metadata, _}} ->
{:error, format_error_reason({metadata, "unexpected_token", key})}

{{:error, :unexpected_token}, {{:atom, key}, _}} ->
{:error, format_error_reason({[], "unexpected_token", key})}

{:error, reason = {_metadata, _message, _cause}} ->
{:error, format_error_reason(reason)}
end
Expand All @@ -251,35 +254,25 @@ defmodule ArchEthic.Contracts.Interpreter do
end

defp format_error_reason({metadata, message, cause}) do
message =
if message == "unexpected token: " do
"unexpected token"
else
message
end
message = prepare_message(message)

line = Keyword.get(metadata, :line)
column = Keyword.get(metadata, :column)

metadata_string = "L#{line}"

metadata_string =
if column == nil do
metadata_string
else
metadata_string <> ":C#{column}"
end
[prepare_message(message), cause, metadata_to_string(metadata)]
|> Enum.reject(&(&1 == ""))
|> Enum.join(" - ")
end

message =
if is_atom(message) do
message |> Atom.to_string() |> String.replace("_", " ")
else
message
end
defp prepare_message(message) when is_atom(message) do
message |> Atom.to_string() |> String.replace("_", " ")
end

"#{message} - #{cause} - #{metadata_string}"
defp prepare_message(message) when is_binary(message) do
String.trim_trailing(message, ":")
end

defp metadata_to_string(line: line, column: column), do: "L#{line}:C#{column}"
defp metadata_to_string(line: line), do: "L#{line}"
defp metadata_to_string(_), do: ""

# Whitelist operators
defp prewalk(node = {:+, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root,
do: {node, acc}
Expand Down Expand Up @@ -675,7 +668,8 @@ defmodule ArchEthic.Contracts.Interpreter do
defp prewalk(
node = [
{{:atom, "public_key"}, _public_key},
{{:atom, "encrypted_secret_key"}, _encrypted_secret_key}
{{:atom, "encrypted_secret_key"}, _encrypted_secret_key},
{{:atom, "secret_index"}, _secret_index}
],
acc = {:ok, %{scope: {:function, "add_authorized_key", :actions}}}
) do
Expand All @@ -686,7 +680,7 @@ defmodule ArchEthic.Contracts.Interpreter do
node = {{:atom, arg}, _},
acc = {:ok, %{scope: {:function, "add_authorized_key", :actions}}}
)
when arg in ["public_key", "encrypted_secret_key"],
when arg in ["public_key", "encrypted_secret_key", "secret_index"],
do: {node, acc}

# Whitelist generics
Expand Down
46 changes: 34 additions & 12 deletions lib/archethic/contracts/interpreter/transaction_statements.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,55 +127,77 @@ defmodule ArchEthic.Contracts.Interpreter.TransactionStatements do
## Examples
iex> TransactionStatements.add_authorized_key(%Transaction{data: %TransactionData{}}, [
...> {"secret_index", 0},
...> {"public_key", "22368B50D3B2976787CFCC27508A8E8C67483219825F998FC9D6908D54D0FE10"},
...> {"encrypted_secret_key", "FB49F76933689ECC9D260D57C2BEF9489234FE72DD2ED1C77E2E8B4E94D9137F"}
...> ])
%Transaction{
data: %TransactionData{
keys: %Keys{
authorized_keys: %{
authorized_keys: [%{
<<34, 54, 139, 80, 211, 178, 151, 103, 135, 207, 204, 39, 80, 138, 142, 140,
103, 72, 50, 25, 130, 95, 153, 143, 201, 214, 144, 141, 84, 208, 254, 16>> =>
<<251, 73, 247, 105, 51, 104, 158, 204, 157, 38, 13, 87, 194, 190, 249, 72, 146,
52, 254, 114, 221, 46, 209, 199, 126, 46, 139, 78, 148, 217, 19, 127>>
}
}]
}
}
}
"""
@spec add_authorized_key(Transaction.t(), list()) :: map()
def add_authorized_key(tx = %Transaction{}, [
{"secret_index", secret_index},
{"public_key", public_key},
{"encrypted_secret_key", encrypted_secret_key}
])
when is_binary(public_key) and is_binary(encrypted_secret_key) do
when is_integer(secret_index) and secret_index >= 0 and is_binary(public_key) and
is_binary(encrypted_secret_key) do
update_in(
tx,
[Access.key(:data), Access.key(:keys), Access.key(:authorized_keys)],
&Map.put(&1, decode_binary(public_key), decode_binary(encrypted_secret_key))
fn authorized_keys ->
case length(authorized_keys) do
0 ->
[%{decode_binary(public_key) => decode_binary(encrypted_secret_key)}]

length when secret_index < length ->
List.update_at(
authorized_keys,
secret_index,
&Map.put(&1, decode_binary(public_key), decode_binary(encrypted_secret_key))
)

length ->
List.update_at(
authorized_keys,
length - 1,
&Map.put(&1, decode_binary(public_key), decode_binary(encrypted_secret_key))
)
end
end
)
end

@doc """
Set the transaction encrypted secret
Add a transaction encrypted secret
## Examples
iex> TransactionStatements.set_secret(%Transaction{data: %TransactionData{}}, "mysecret")
iex> TransactionStatements.add_secret(%Transaction{data: %TransactionData{}}, "mysecret")
%Transaction{
data: %TransactionData{
keys: %Keys{
secret: "mysecret"
secrets: ["mysecret"]
}
}
}
"""
@spec set_secret(Transaction.t(), binary()) :: Transaction.t()
def set_secret(tx = %Transaction{}, secret) when is_binary(secret) do
put_in(
@spec add_secret(Transaction.t(), binary()) :: Transaction.t()
def add_secret(tx = %Transaction{}, secret) when is_binary(secret) do
update_in(
tx,
[Access.key(:data), Access.key(:keys), Access.key(:secret)],
decode_binary(secret)
[Access.key(:data), Access.key(:keys), Access.key(:secrets)],
&(&1 ++ [decode_binary(secret)])
)
end

Expand Down
65 changes: 44 additions & 21 deletions lib/archethic/contracts/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,6 @@ defmodule ArchEthic.Contracts.Worker do
next_tx,
prev_tx = %Transaction{
address: address,
data: %TransactionData{keys: %Keys{authorized_keys: authorized_keys, secret: secret}},
previous_public_key: previous_public_key
}
) do
Expand All @@ -273,19 +272,44 @@ defmodule ArchEthic.Contracts.Worker do
|> chain_secret()
|> chain_authorized_keys()

# TODO: improve transaction decryption and transaction signing to avoid the reveal of the transaction seed
with encrypted_key <- Map.get(authorized_keys, Crypto.storage_nonce_public_key()),
{:ok, aes_key} <- Crypto.ec_decrypt_with_storage_nonce(encrypted_key),
{:ok, transaction_seed} <- Crypto.aes_decrypt(secret, aes_key),
length <- TransactionChain.size(address) do
{:ok,
Transaction.new(
new_type,
new_data,
transaction_seed,
length,
Crypto.get_public_key_curve(previous_public_key)
)}
case get_transaction_seed(prev_tx) do
{:ok, transaction_seed} ->
length = TransactionChain.size(address)

{:ok,
Transaction.new(
new_type,
new_data,
transaction_seed,
length,
Crypto.get_public_key_curve(previous_public_key)
)}

_ ->
Logger.error("Cannot decrypt the transaction seed", contract: Base.encode16(address))
{:error, :transaction_seed_decryption}
end
end

defp get_transaction_seed(%Transaction{
data: %TransactionData{keys: %Keys{secrets: secrets, authorized_keys: authorized_keys}}
}) do
storage_nonce_public_key = Crypto.storage_nonce_public_key()

secret_index =
Enum.find_index(authorized_keys, fn authorized_keys_by_secret ->
authorized_keys_by_secret
|> Map.keys()
|> Enum.any?(&(&1 == storage_nonce_public_key))
end)

encrypted_key = authorized_keys |> Enum.at(secret_index) |> Map.get(storage_nonce_public_key)

secret_for_node = Enum.at(secrets, secret_index)

with {:ok, aes_key} <- Crypto.ec_decrypt_with_storage_nonce(encrypted_key),
{:ok, transaction_seed} <- Crypto.aes_decrypt(secret_for_node, aes_key) do
{:ok, transaction_seed}
end
end

Expand Down Expand Up @@ -328,16 +352,16 @@ defmodule ArchEthic.Contracts.Worker do

defp chain_secret(
acc = %{
next_transaction: %Transaction{data: %TransactionData{keys: %Keys{secret: ""}}},
next_transaction: %Transaction{data: %TransactionData{keys: %Keys{secrets: []}}},
previous_transaction: %Transaction{
data: %TransactionData{keys: %Keys{secret: previous_secret}}
data: %TransactionData{keys: %Keys{secrets: previous_secrets}}
}
}
) do
put_in(
acc,
[:next_transaction, Access.key(:data, %{}), Access.key(:keys, %{}), Access.key(:secret)],
previous_secret
[:next_transaction, Access.key(:data, %{}), Access.key(:keys, %{}), Access.key(:secrets)],
previous_secrets
)
end

Expand All @@ -346,14 +370,13 @@ defmodule ArchEthic.Contracts.Worker do
defp chain_authorized_keys(
acc = %{
next_transaction: %Transaction{
data: %TransactionData{keys: %Keys{authorized_keys: authorized_keys}}
data: %TransactionData{keys: %Keys{authorized_keys: []}}
},
previous_transaction: %Transaction{
data: %TransactionData{keys: %Keys{authorized_keys: previous_authorized_keys}}
}
}
)
when map_size(authorized_keys) == 0 do
) do
put_in(
acc,
[
Expand Down
4 changes: 2 additions & 2 deletions lib/archethic/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,7 @@ defmodule ArchEthic.Crypto do
def load_transaction(%Transaction{
address: address,
type: :node_shared_secrets,
data: %TransactionData{keys: keys = %Keys{secret: secret}},
data: %TransactionData{keys: keys = %Keys{secrets: [secret]}},
validation_stamp: %ValidationStamp{
timestamp: timestamp
}
Expand All @@ -986,7 +986,7 @@ defmodule ArchEthic.Crypto do
)

if Keys.authorized_key?(keys, last_node_public_key()) do
encrypted_secret_key = Keys.get_encrypted_key(keys, last_node_public_key())
encrypted_secret_key = Keys.get_encrypted_key_at(keys, 0, last_node_public_key())

daily_nonce_date = SharedSecrets.next_application_date(timestamp)

Expand Down
4 changes: 2 additions & 2 deletions lib/archethic/crypto/keystore/shared_secrets/software_impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ defmodule ArchEthic.Crypto.SharedSecretsKeystore.SoftwareImpl do

%Transaction{
address: address,
data: %TransactionData{keys: keys = %Keys{secret: secret}},
data: %TransactionData{keys: keys = %Keys{secrets: [secret]}},
validation_stamp: %ValidationStamp{timestamp: timestamp}
} ->
if Keys.authorized_key?(keys, Crypto.last_node_public_key()) do
encrypted_secret_key = Keys.get_encrypted_key(keys, Crypto.last_node_public_key())
encrypted_secret_key = Keys.get_encrypted_key_at(keys, 0, Crypto.last_node_public_key())

daily_nonce_date = SharedSecrets.next_application_date(timestamp)

Expand Down
4 changes: 2 additions & 2 deletions lib/archethic/election.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ defmodule ArchEthic.Election do
...> )
[
%Node{last_public_key: "node6", geo_patch: "ECA"},
%Node{last_public_key: "node5", geo_patch: "F10"},
%Node{last_public_key: "node1", geo_patch: "AAA"},
%Node{last_public_key: "node2", geo_patch: "DEF"},
%Node{last_public_key: "node3", geo_patch: "AA0"},
%Node{last_public_key: "node5", geo_patch: "F10"},
]
"""
@spec validation_nodes(
Expand Down
Loading

0 comments on commit a4b409b

Please sign in to comment.