Skip to content

Instantly share code, notes, and snippets.

@mogorman
Created September 1, 2022 22:43
Show Gist options
  • Save mogorman/8838c7e520a0ad2a7856e601ab1a6afd to your computer and use it in GitHub Desktop.
Save mogorman/8838c7e520a0ad2a7856e601ab1a6afd to your computer and use it in GitHub Desktop.
teller.io challenge

Teller Bank Challenge

Mix.install([:req, :jason, :kino])
Resolving Hex dependencies...
Dependency resolution completed:
New:
  castore 0.1.18
  finch 0.13.0
  hpax 0.1.2
  jason 1.3.0
  kino 0.6.2
  mime 2.0.3
  mint 1.4.2
  nimble_options 0.4.0
  nimble_pool 0.2.6
  req 0.3.0
  table 0.1.2
  telemetry 1.1.0
* Getting req (Hex package)
* Getting jason (Hex package)
* Getting kino (Hex package)
* Getting table (Hex package)
* Getting finch (Hex package)
* Getting mime (Hex package)
* Getting castore (Hex package)
* Getting mint (Hex package)
* Getting nimble_options (Hex package)
* Getting nimble_pool (Hex package)
* Getting telemetry (Hex package)
* Getting hpax (Hex package)
==> nimble_options
Compiling 3 files (.ex)
Generated nimble_options app
==> hpax
Compiling 4 files (.ex)
Generated hpax app
==> nimble_pool
Compiling 2 files (.ex)
Generated nimble_pool app
===> Analyzing applications...
===> Compiling telemetry
==> jason
Compiling 10 files (.ex)
Compiling lib/decoder.ex (it's taking more than 10s)
Generated jason app
==> table
Compiling 5 files (.ex)
Generated table app
==> kino
Compiling 28 files (.ex)
Generated kino app
==> castore
Compiling 1 file (.ex)
Generated castore app
==> mint
Compiling 1 file (.erl)
Compiling 19 files (.ex)
Generated mint app
==> mime
Compiling 1 file (.ex)
Generated mime app
==> finch
Compiling 13 files (.ex)
Generated finch app
==> req
Compiling 5 files (.ex)
Generated req 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
    @type username() :: String.t()
    @type password() :: String.t()

    def build_f_token(f_token_spec, username, device_id, api_key, last_request_id) do
      encoded =
        f_token_spec
        |> Base.decode64!()
        |> Jason.decode!()
        |> Map.get("values")
        |> Enum.map(fn
          "last-request-id" -> last_request_id
          "username" -> username
          "device-id" -> device_id
          "api-key" -> api_key
        end)
        |> Enum.join(":")

      f_token =
        :sha256
        |> :crypto.hash(encoded)
        |> Base.encode64(padding: false)
    end

    @spec fetch(username, password) :: ChallengeResult.t()
    def fetch(username, password) do
      device_id = "CTK7P2CQKSQS4ZER"
      api_key = "good-luck-at-the-teller-quiz!"
      user_agent = "Teller Bank iOS 1.0"

      headers = [
        user_agent: user_agent,
        api_key: api_key,
        device_id: device_id,
        accept: "application/json"
      ]

      result =
        Req.post!("https://challenge.teller.engineering/login",
          headers: headers,
          json: %{"username" => username, "password" => password}
        )

      sms_code =
        result
        |> Map.get(:body)
        |> Map.get("mfa_devices")
        |> Enum.find(fn mfa -> Map.get(mfa, "type") == "SMS" end)
        |> Map.get("id")

      result
      result_headers = Map.new(result.headers)
      last_request_id = Map.get(result_headers, "f-request-id", "")

      f_token =
        result_headers
        |> Map.get("f-token-spec")
        |> build_f_token(username, device_id, api_key, last_request_id)

      result =
        Req.post!("https://challenge.teller.engineering/login/mfa/request",
          headers:
            headers ++
              [
                request_token: Map.get(result_headers, "request-token"),
                f_token: f_token,
                teller_is_hiring: "I know!"
              ],
          json: %{"device_id" => sms_code}
        )

      result_headers = Map.new(result.headers)
      last_request_id = Map.get(result_headers, "f-request-id", "")

      f_token =
        result_headers
        |> Map.get("f-token-spec")
        |> build_f_token(username, device_id, api_key, last_request_id)

      result =
        Req.post!("https://challenge.teller.engineering/login/mfa",
          headers:
            headers ++
              [
                request_token: Map.get(result_headers, "request-token"),
                f_token: f_token,
                teller_is_hiring: "I know!"
              ],
          json: %{"code" => TellerBank.OTPCode.generate(username)}
        )

      result_headers = Map.new(result.headers)
      last_request_id = Map.get(result_headers, "f-request-id", "")

      f_token =
        result_headers
        |> Map.get("f-token-spec")
        |> build_f_token(username, device_id, api_key, last_request_id)

      account =
        result.body
        |> Map.get("accounts")
        |> Map.get("checking")
        |> Enum.at(0)
        |> Map.get("id")

      result =
        Req.get!("https://challenge.teller.engineering/accounts/#{account}/details",
          headers:
            headers ++
              [
                request_token: Map.get(result_headers, "request-token"),
                f_token: f_token,
                teller_is_hiring: "I know!"
              ]
        )

      full_account_id =
        result.body
        |> Map.get("number")

      result_headers = Map.new(result.headers)
      last_request_id = Map.get(result_headers, "f-request-id", "")

      f_token =
        result_headers
        |> Map.get("f-token-spec")
        |> build_f_token(username, device_id, api_key, last_request_id)

      result =
        Req.get!("https://challenge.teller.engineering/accounts/#{account}/balances",
          headers:
            headers ++
              [
                request_token: Map.get(result_headers, "request-token"),
                f_token: f_token,
                teller_is_hiring: "I know!"
              ]
        )

      amount = Map.get(result.body, "available")
      {full_account_id, amount}
      %ChallengeResult{account_number: full_account_id, balance_in_cents: amount}
    end
  end
end

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

TellerBank.Client.fetch(username, password)
warning: variable "f_token" is unused (if the variable is not meant to be used, prefix it with an underscore)
  #cell:54: TellerBank.Client.build_f_token/5

warning: variable result in code block has no effect as it is never returned (remove the variable or assign it to _ to avoid warnings)
  #cell:78: TellerBank.Client.fetch/2

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