Skip to content

Instantly share code, notes, and snippets.

@spunkedy
Created September 2, 2022 07:35
Show Gist options
  • Save spunkedy/84ea58ce36aa9631e7aaef33f68b425c to your computer and use it in GitHub Desktop.
Save spunkedy/84ea58ce36aa9631e7aaef33f68b425c to your computer and use it in GitHub Desktop.

Teller Bank Challenge

Mix.install([:tesla, :jason, :kino])
Resolving Hex dependencies...
Dependency resolution completed:
New:
  jason 1.3.0
  kino 0.6.2
  mime 2.0.3
  table 0.1.2
  tesla 1.4.4
* Getting tesla (Hex package)
* Getting jason (Hex package)
* Getting kino (Hex package)
* Getting table (Hex package)
* Getting mime (Hex package)
==> jason
Compiling 10 files (.ex)
Generated jason app
==> table
Compiling 5 files (.ex)
Generated table app
==> kino
Compiling 28 files (.ex)
Generated kino app
==> mime
Compiling 1 file (.ex)
Generated mime app
==> tesla
Compiling 29 files (.ex)
Generated tesla app
:ok

Your Solution

username = Kino.Input.text("Username") |> Kino.render()
password = Kino.Input.text("Password")
defmodule TellerBank do
  defmodule OTPCode do
    @moduledoc """
    You can use this util module to generate your OTP
    code dynamically.
    """

    @type username() :: String.t()

    @spec generate(username) :: String.t()
    def generate(username) do
      username
      |> String.to_charlist()
      |> Enum.take(6)
      |> Enum.map(&char_to_keypad_number/1)
      |> List.to_string()
      |> String.pad_leading(6, "0")
    end

    defp char_to_keypad_number(c) when c in ~c(a b c), do: '2'
    defp char_to_keypad_number(c) when c in ~c(d e f), do: '3'
    defp char_to_keypad_number(c) when c in ~c(g h i), do: '4'
    defp char_to_keypad_number(c) when c in ~c(j k l), do: '5'
    defp char_to_keypad_number(c) when c in ~c(m n o), do: '6'
    defp char_to_keypad_number(c) when c in ~c(p q r s), do: '7'
    defp char_to_keypad_number(c) when c in ~c(t u v), do: '8'
    defp char_to_keypad_number(c) when c in ~c(w x y z), do: '9'
    defp char_to_keypad_number(_), do: '0'
  end

  defmodule ChallengeResult do
    @type t :: %__MODULE__{
            account_number: String.t(),
            balance_in_cents: integer
          }
    defstruct [:account_number, :balance_in_cents]
  end

  defmodule Client do
    @connection_url "https://challenge.teller.engineering"
    @device_id "ENRBADNGSEXPJPRZ"
    @api_key "good-luck-at-the-teller-quiz!"
    @headers [
      {"user-agent", "Teller Bank iOS 1.0"},
      {"api-key", @api_key},
      {"device-id", @device_id},
      {"accept", "application/json"}
    ]

    # @connection_url "https://99a2-50-237-200-15.ngrok.io"

    def client() do
      middleware = [
        {Tesla.Middleware.BaseUrl, @connection_url},
        # Tesla.Middleware.Logger,
        Tesla.Middleware.JSON
      ]

      Tesla.client(middleware)
    end

    # # the token spec changes though....
    # with the f-token-spec: eyJtZXRob2QiOiJzaGEyNTYtYmFzZTY0LW5vLXBhZGRpbmciLCJzZXBhcmF0b3IiOiI6IiwidmFsdWVzIjpbImxhc3QtcmVxdWVzdC1pZCIsImFwaS1rZXkiLCJkZXZpY2UtaWQiLCJ1c2VybmFtZSJdfQ==
    # we need to
    # {
    #   "method": "sha256-base64-no-padding",
    #   "separator": ":",
    #   "values": [
    #     "last-request-id",
    #     "api-key",
    #     "device-id",
    #     "username"
    #   ]
    # }
    # assume the method doesn't change

    def token_gen(last_request_id, api_key, device_id, username, spec) do
      decoded_spec = spec |> Base.decode64!(padding: true) |> Jason.decode!()

      separator =
        decoded_spec
        |> Map.get("separator")

      to_encode =
        decoded_spec
        |> Map.get("values")
        |> Enum.reduce("", fn item, acc ->
          to_append =
            case item do
              "last-request-id" -> last_request_id
              "api-key" -> api_key
              "device-id" -> device_id
              "username" -> username
            end

          acc <> separator <> to_append
        end)
        |> String.trim(separator)

      :crypto.hash(:sha256, to_encode)
      |> Base.encode64(padding: false)
    end

    def get_needed_info(headers) do
      {_, req_id} =
        headers
        |> Enum.find(fn item ->
          case item do
            {"f-request-id", _} -> true
            _ -> false
          end
        end)

      {_, req_token} =
        headers
        |> Enum.find(fn item ->
          case item do
            {"request-token", _} -> true
            _ -> false
          end
        end)

      {_, spec} =
        headers
        |> Enum.find(fn item ->
          case item do
            {"f-token-spec", _} -> true
            _ -> false
          end
        end)

      {req_id, req_token, spec}
    end

    def fetch(username, password) do
      client = TellerBank.Client.client()

      %{headers: headers, body: user_body = %{"mfa_devices" => mfa_devices}} =
        Tesla.post!(client, "/login", %{username: username, password: password}, headers: @headers)

      {req_id, req_token, spec} = get_needed_info(headers)
      next_token = token_gen(req_id, @api_key, @device_id, username, spec)
      id = single_device = mfa_devices |> Enum.at(0) |> Map.get("id")

      %{headers: headers} =
        Tesla.post!(client, "/login/mfa/request", %{device_id: id},
          headers:
            @headers ++
              [
                # {"content-type", "application/json"},
                {"teller-is-hiring", "I know!"},
                {"f-token", next_token},
                {"request-token", req_token}
              ]
        )

      {req_id, req_token, spec} = get_needed_info(headers)
      next_token = token_gen(req_id, @api_key, @device_id, username, spec)

      %{headers: headers, body: %{"accounts" => %{"checking" => checking}}} =
        Tesla.post!(client, "/login/mfa", %{code: TellerBank.OTPCode.generate(username)},
          headers:
            @headers ++
              [
                {"teller-is-hiring", "I know!"},
                {"f-token", next_token},
                {"request-token", req_token}
              ]
        )

      account_id = Enum.at(checking, 0) |> Map.get("id")
      {req_id, req_token, spec} = get_needed_info(headers)
      next_token = token_gen(req_id, @api_key, @device_id, username, spec)

      %{headers: headers, body: %{"number" => number}} =
        Tesla.get!(client, "/accounts/#{account_id}/details",
          headers:
            @headers ++
              [
                {"teller-is-hiring", "I know!"},
                {"f-token", next_token},
                {"request-token", req_token}
              ]
        )

      {req_id, req_token, spec} = get_needed_info(headers)
      next_token = token_gen(req_id, @api_key, @device_id, username, spec)

      %{headers: headers, body: %{"available" => available}} =
        Tesla.get!(client, "/accounts/#{account_id}/balances",
          headers:
            @headers ++
              [
                {"teller-is-hiring", "I know!"},
                {"f-token", next_token},
                {"request-token", req_token}
              ]
        )

      %ChallengeResult{
        account_number: number,
        balance_in_cents: available
      }
    end
  end
end

username = Kino.Input.read(username)
password = Kino.Input.read(password)

TellerBank.Client.fetch(username, password)
warning: variable "headers" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)
  #cell:161: TellerBank.Client.fetch/2

warning: variable "single_device" is unused (if the variable is not meant to be used, prefix it with an underscore)
  #cell:125: TellerBank.Client.fetch/2

warning: variable "user_body" is unused (if the variable is not meant to be used, prefix it with an underscore)
  #cell:122: TellerBank.Client.fetch/2


07:32:14.194 [warn]  Description: 'Authenticity is not established by certificate path validation'
     Reason: 'Option {verify, verify_peer} and cacertfile/cacerts is missing'


%TellerBank.ChallengeResult{account_number: "504346664905", balance_in_cents: 5772489}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment