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

Smart Contracts: Crypto.encrypt/2 & Crypto.encrypt_with_storage_nonce/1 #1530

Closed
Closed
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
34 changes: 34 additions & 0 deletions lib/archethic/contracts/interpreter/library/common/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,31 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.Crypto do
end
end

@doc """
This function uses the process dictionnary to have a fixed entropy.
This is required because the ec_encrypt would not be determinist without it.

Caller is supposed to pass the hash(contract_tx.private_key)
"""
@spec encrypt(data :: binary(), public_key :: binary()) :: binary()
def encrypt(data, public_key) do
data = UtilsInterpreter.maybe_decode_hex(data)
public_key = UtilsInterpreter.maybe_decode_hex(public_key)

Archethic.Crypto.ec_encrypt(
data,
public_key,
:crypto.hash(:sha256, Process.get(:ephemeral_entropy_priv_key))
)
end

@spec encrypt_with_storage_nonce(data :: binary()) :: binary()
def encrypt_with_storage_nonce(data) do
data
|> UtilsInterpreter.maybe_decode_hex()
|> Crypto.ec_encrypt_with_storage_nonce()
end

@spec check_types(atom(), list()) :: boolean()
def check_types(:hash, [first]) do
AST.is_binary?(first) || AST.is_variable_or_function_call?(first)
Expand Down Expand Up @@ -142,5 +167,14 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.Crypto do
AST.is_binary?(first) || AST.is_variable_or_function_call?(first)
end

def check_types(:encrypt, [first, second]) do
(AST.is_binary?(first) || AST.is_variable_or_function_call?(first)) &&
(AST.is_binary?(second) || AST.is_variable_or_function_call?(second))
end

def check_types(:encrypt_with_storage_nonce, [first]) do
AST.is_binary?(first) || AST.is_variable_or_function_call?(first)
end

def check_types(_, _), do: false
end
37 changes: 32 additions & 5 deletions lib/archethic/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,17 @@ defmodule Archethic.Crypto do
pub
end

@doc """
Encrypt given data with the storage nonce public key.

More details at `ec_encrypt/3`
"""
@spec ec_encrypt_with_storage_nonce(iodata()) :: binary()
def ec_encrypt_with_storage_nonce(data, ephemeral_entropy_priv_key \\ nil)
when is_bitstring(data) or is_list(data) do
ec_encrypt(data, storage_nonce_public_key(), ephemeral_entropy_priv_key)
end

@doc """
Decrypt a cipher using the storage nonce public key using an authenticated encryption (ECIES).

Expand Down Expand Up @@ -585,14 +596,24 @@ defmodule Archethic.Crypto do
40, 0, 68, 224, 177, 110, 180, 24>>
```
"""
@spec ec_encrypt(message :: binary(), public_key :: key()) :: binary()
def ec_encrypt(message, <<curve_id::8, _::8, public_key::binary>> = _public_key)
@spec ec_encrypt(
message :: binary(),
public_key :: key(),
ephemeral_entropy_priv_key :: binary() | nil
) ::
binary()
def ec_encrypt(
message,
<<curve_id::8, _::8, public_key::binary>> = _public_key,
ephemeral_entropy_priv_key \\ nil
)
when is_binary(message) do
start_time = System.monotonic_time()

curve = ID.to_curve(curve_id)

{ephemeral_public_key, ephemeral_private_key} = generate_ephemeral_encryption_keys(curve)
{ephemeral_public_key, ephemeral_private_key} =
generate_ephemeral_encryption_keys(curve, ephemeral_entropy_priv_key)

# Derivate secret using ECDH with the given public key and the ephemeral private key
shared_key =
Expand All @@ -618,8 +639,14 @@ defmodule Archethic.Crypto do
<<ephemeral_public_key::binary, tag::binary, cipher::binary>>
end

defp generate_ephemeral_encryption_keys(:ed25519), do: :crypto.generate_key(:ecdh, :x25519)
defp generate_ephemeral_encryption_keys(curve), do: :crypto.generate_key(:ecdh, curve)
defp generate_ephemeral_encryption_keys(:ed25519, ephemeral_entropy_priv_key),
do: generate_ephemeral_encryption_keys(:x25519, ephemeral_entropy_priv_key)

defp generate_ephemeral_encryption_keys(curve, nil),
do: :crypto.generate_key(:ecdh, curve)

defp generate_ephemeral_encryption_keys(curve, ephemeral_entropy_priv_key),
do: :crypto.generate_key(:ecdh, curve, ephemeral_entropy_priv_key)

defp derivate_secrets(dh_key) do
pseudorandom_key = :crypto.hash(:sha256, dh_key)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,39 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.CryptoTest do
end
end

describe "encrypt/2" do
test "should encrypt deterministically the given data with given public key" do
data = :crypto.strong_rand_bytes(10)
{pub, _} = Archethic.Crypto.derive_keypair("seed", 0)
{_, entropy_priv_key} = Archethic.Crypto.generate_deterministic_keypair("bioman")

expected_cipher =
Archethic.Crypto.ec_encrypt(data, pub, :crypto.hash(:sha256, entropy_priv_key))

Process.put(:ephemeral_entropy_priv_key, entropy_priv_key)

assert ^expected_cipher = Crypto.encrypt(data, Base.encode16(pub))
end
end

describe "encrypt_with_storage_nonce/decrypt_with_storage_nonce" do
test "should return the same value once encrypted/decrypted (binary)" do
data = :crypto.strong_rand_bytes(10)
assert ^data = Crypto.decrypt_with_storage_nonce(Crypto.encrypt_with_storage_nonce(data))
end

test "should return the same value once encrypted/decrypted (string)" do
data = "hello uco"
assert ^data = Crypto.decrypt_with_storage_nonce(Crypto.encrypt_with_storage_nonce(data))
end

# ERRONOUS because of the maybe_decode_hex
# test "should return the same value once encrypted/decrypted (hex)" do
# data = "abcdef"
# assert ^data = Crypto.decrypt_with_storage_nonce(Crypto.encrypt_with_storage_nonce(data))
# end
end

describe "decrypt_with_storage_nonce" do
test "should raise when the ciphertext cannot be decrypted by the storage nonce" do
assert_raise Library.Error, fn -> Crypto.decrypt_with_storage_nonce("123456") end
Expand Down
52 changes: 52 additions & 0 deletions test/archethic/crypto_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ defmodule CryptoTest do

doctest Crypto

setup_all do
MockCrypto.SharedSecretsKeystore
|> stub(:get_storage_nonce, fn ->
{_, pv} = Crypto.generate_deterministic_keypair("nonce")
pv
end)

:ok
end

test "giving a seed always result in the same result" do
ephemeral_entropy_priv_key = :crypto.strong_rand_bytes(32)
{pub, _} = Crypto.generate_deterministic_keypair("seed", :secp256r1)

assert Crypto.ec_encrypt("msg", pub) != Crypto.ec_encrypt("msg", pub)

assert Crypto.ec_encrypt("msg", pub, ephemeral_entropy_priv_key) ==
Crypto.ec_encrypt("msg", pub, ephemeral_entropy_priv_key)
end

property "symmetric aes encryption and decryption" do
check all(
aes_key <- StreamData.binary(length: 32),
Expand All @@ -35,6 +55,26 @@ defmodule CryptoTest do
end
end

property "symmetric EC encryption and decryption with storage nonce" do
check all(data <- StreamData.binary(min_length: 1)) do
cipher = Crypto.ec_encrypt_with_storage_nonce(data)
assert is_binary(cipher)
assert {:ok, ^data} = Crypto.ec_decrypt_with_storage_nonce(cipher)
end
end

property "symmetric EC encryption and decryption with ECDSA (with fixed ephemeral_entropy_priv_key)" do
check all(
seed <- StreamData.binary(length: 32),
data <- StreamData.binary(min_length: 1),
ephemeral_entropy_priv_key <- StreamData.binary(length: 32)
) do
{pub, pv} = Crypto.generate_deterministic_keypair(seed, :secp256r1)
cipher = Crypto.ec_encrypt(data, pub, :crypto.hash(:sha256, ephemeral_entropy_priv_key))
is_binary(cipher) and data == Crypto.ec_decrypt!(cipher, pv)
end
end

property "symmetric EC encryption and decryption with Ed25519" do
check all(
seed <- StreamData.binary(length: 32),
Expand All @@ -46,6 +86,18 @@ defmodule CryptoTest do
end
end

property "symmetric EC encryption and decryption with Ed25519 (with fixed ephemeral_entropy_priv_key)" do
check all(
seed <- StreamData.binary(length: 32),
data <- StreamData.binary(min_length: 1),
ephemeral_entropy_priv_key <- StreamData.binary(length: 32)
) do
{pub, pv} = Crypto.generate_deterministic_keypair(seed, :ed25519)
cipher = Crypto.ec_encrypt(data, pub, :crypto.hash(:sha256, ephemeral_entropy_priv_key))
is_binary(cipher) and data == Crypto.ec_decrypt!(cipher, pv)
end
end

test "decrypt_and_set_storage_nonce/1 should decrypt storage nonce using node last key and and load storage nonce" do
storage_nonce = :crypto.strong_rand_bytes(32)

Expand Down
Loading