lib/stensitive.ex

defmodule StellarServer do
  def getAccount(pk, horizon_base) do
    url = "https://#{horizon_base}/accounts/#{pk}"
    case HTTPoison.get(url) do
      {:ok, %{status_code: 200, body: body}} ->
        Poison.decode(body)

      {:ok, %{status_code: 400}} ->
        "Account was not found on the specified Horizon API"
    end
  end
end


defmodule Stensitive do
  @moduledoc """
  Documentation for `Stensitive`.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Stensitive.hello()
      :world

  """
  def hello do
    :world
  end

  defp getEncryptionTX(public_key, secretKey, pin_code) do
    time_bounds = Stellar.TxBuild.TimeBounds.new(
      min_time: 1,
      max_time: 1
    )
    base_fee = Stellar.TxBuild.BaseFee.new(1)
    manageData_op = Stellar.TxBuild.ManageData.new(
      entry_name: "why this OP",
      entry_value: "we need this transaction to have one operation.",
    )

    seq_num = pin_code + 1
sequence_number = Stellar.TxBuild.SequenceNumber.new(seq_num)
    account = Stellar.TxBuild.Account.new(public_key)
    {:ok, tx} = account
    |> Stellar.TxBuild.new()
    |> Stellar.TxBuild.set_time_bounds(time_bounds)
    |> Stellar.TxBuild.set_base_fee(base_fee)
    |> Stellar.TxBuild.set_sequence_number(sequence_number)
    |> Stellar.TxBuild.add_operation(manageData_op)
    |> Stellar.TxBuild.sign(Stellar.TxBuild.Signature.new({public_key, secretKey}))
    |> Stellar.TxBuild.envelope()
    tx
  end

  defp extractSignature(tx) do
    tx = Stellar.TxBuild.TransactionEnvelope.to_base64(tx)
    signature = Enum.at(Stellar.TxBuild.TransactionEnvelope.from_base64(tx).envelope.signatures.signatures, 0).signature.signature
    
    |> :base64.encode
    {:ok, signature}
  end

  defp manageDataTX(public_key, secretKey, key, value) do
    source_account = Stellar.TxBuild.Account.new(public_key)
    {:ok, seq_num} = Stellar.Horizon.Accounts.fetch_next_sequence_number(public_key)
    sequence_number = Stellar.TxBuild.SequenceNumber.new(seq_num)
    operation = Stellar.TxBuild.ManageData.new(name: "test", value: "trst")
    signer_key_pair = Stellar.KeyPair.from_secret_seed(secretKey)
    signature = Stellar.TxBuild.Signature.new(signer_key_pair)
    manageData_op = Stellar.TxBuild.ManageData.new(
      entry_name: key,
      entry_value: value,
    )

    {:ok, base64_envelope} =
      source_account
      |> Stellar.TxBuild.new(sequence_number: sequence_number)
      |> Stellar.TxBuild.add_operation(manageData_op)
      |> Stellar.TxBuild.sign(signature)
      |> Stellar.TxBuild.envelope()
    
    {:ok, submitted_tx} = Stellar.Horizon.Transactions.create(base64_envelope)
    submitted_tx
    
  end

  defp ipfsUpload(dataName, data) do
    content = data
    url = "https://ipfs.tdep.workers.dev/"
    headers = [{"Content-type", "application/json"}]
    body = Poison.encode!(%{dataName: dataName , data: content})
    case HTTPoison.post(url, body, headers, []) do
      {:ok, %{status_code: 200, body: ipfshash}} ->
	ipfshash
      {:ok, %{status_code: 400}} ->
	"IPFS upload failed"
    end
  end

  def ipfsDownload(ipfshash) do
    url = "https://gateway.pinata.cloud/ipfs/" <> ipfshash
    case HTTPoison.get(url) do

      {:ok, %{status_code: 200, body: data}} ->
	Map.get(Poison.decode!(data), "data")
      {:ok, %{status_code: 400}} ->
	"IPFS download failed"
    end
  end

  defp pad(data, size) do
    padding = size - rem(byte_size(data), size)
    data <> :binary.copy(<<padding>>, padding)
  end
  
  def aes_encrypt(text, key) do
    mode = :aes_256_cbc
    a = :crypto.hash(:md5, key)
    b = :crypto.hash(:md5, a <> key)
    c = :crypto.hash(:md5, b <> key)
    iv = a <> b
    key = c
    encrypted = :crypto.crypto_one_time(mode, iv, key, text, [encrypt: true, padding: :pkcs_padding]) 
    {:ok, Base.encode16(encrypted, case: :lower)}
  end

  def aes_decrypt(encrypted, key) do
    mode = :aes_256_cbc
    encrypted = Base.decode16!(encrypted, case: :lower)
    a = :crypto.hash(:md5, key)
    b = :crypto.hash(:md5, a <> key)
    c = :crypto.hash(:md5, b <> key)
    iv = a <> b
    key = c
    decrypted = :crypto.crypto_one_time(mode, iv, key, encrypted, [encrypt: false, padding: :pkcs_padding] )
    decrypted_text = Base.encode16(decrypted, case: :lower)
    
    {:ok, decrypted_text}
  end

  defp getEncryptedData(publicKey, dataName) do
    {:ok, data} = StellarServer.getAccount(publicKey, "horizon-testnet.stellar.org") # For now, only testnet is supported for the official library
    dataAttrs = Map.get(data, "data")
    ipfsHash = Map.get(dataAttrs, dataName) |> :base64.decode
    encryptedData = ipfsDownload(ipfsHash)
    {:ok, encryptedData}
  end
  
  def encrypt(dataName, content, pin_code, walletName, password) do
    IO.puts("... Encrypting data...")
    {:ok, secretKey} = ExSWallet.loadSecret(walletName, password)
      {publicKey, secretKey} = Stellar.KeyPair.from_secret_seed(secretKey)
      tx = getEncryptionTX(publicKey, secretKey, pin_code)
      signed_tx = Stellar.TxBuild.TransactionEnvelope.from_base64(tx)
      {:ok, signature} = extractSignature(signed_tx)
      {:ok, to_upload} = aes_encrypt(content, signature)
      IO.puts("... Uploading data ...")
      ipfs_hash = ipfsUpload(dataName, to_upload)
      manageDataTX(publicKey, secretKey, dataName, ipfs_hash)
      IO.puts("Data encrypted and saved on Stellar")
  end

  def decrypt(dataName, pin_code, walletName, password) do
    IO.puts("... Downloading data ...")
    {:ok, secretKey} = ExSWallet.loadSecret(walletName, password)
    {publicKey, secretKey} = Stellar.KeyPair.from_secret_seed(secretKey)
    tx = getEncryptionTX(publicKey, secretKey, pin_code)
    signed_tx = Stellar.TxBuild.TransactionEnvelope.from_base64(tx)
    {:ok, signature} = extractSignature(signed_tx)
    {:ok,  encryptedData}  = getEncryptedData(publicKey, dataName)
    IO.puts("... Decrypting data ...")
    {:ok, decryptedData} = aes_decrypt(encryptedData, signature)
    dd = Base.decode16!(decryptedData, case: :lower)
    {:ok, dd}
  end
end