Skip to content

Instantly share code, notes, and snippets.

@hailelagi
Last active November 4, 2022 03:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hailelagi/b260b8546843b635208d24306e64aa00 to your computer and use it in GitHub Desktop.
Save hailelagi/b260b8546843b635208d24306e64aa00 to your computer and use it in GitHub Desktop.
Teller Challenge for Code BEAM America

Teller Bank Challenge

Mix.install([:req, :jason, :kino])

Solution

username = Kino.Input.text("Username") |> Kino.render()
password = Kino.Input.text("Password")
defmodule TellerBank do
  defmodule ChallengeResult do
    @type t :: %__MODULE__{
            account_number: String.t(),
            balance_in_cents: integer
          }
    defstruct [:account_number, :balance_in_cents]
  end

  defmodule Client do
    @url "https://challenge.teller.engineering/"
    @device_id "E7XRJC6WWGMXSZAE"
    @api_key "HelloMountainView!"

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

    @spec fetch(username, password) :: ChallengeResult.t()
    def fetch(username, password) do
      with {:ok, %Req.Response{body: body} = res} <- Client.login(username, password),
           {:ok, res} <- Client.mfa_request(body, res),
           {:ok, %Req.Response{body: body} = res} <- Client.mfa_auth(res),
           {:ok, %Req.Response{body: balances} = res} <- Client.account(:balance, body, res),
           {:ok, %Req.Response{body: acc}} <- Client.account(:details, body, res) do
        %ChallengeResult{
          account_number: TellerBank.decrypt_account(body["enc_session_key"], acc["number"]),
          balance_in_cents: balances
        }
      end
    end

    def login(username, password) do
      body = %{username: username, password: password}
      headers = ios_headers() ++ content_header()

      Req.post(@url <> "login", json: body, headers: headers)
    end

    def mfa_request(body, response) do
      [f_token_spec] = Req.Response.get_header(response, "f-token-spec")
      [request_token] = Req.Response.get_header(response, "request-token")
      [request_id] = Req.Response.get_header(response, "f-request-id")

      body = %{device_id: List.last(body["devices"])["id"]}

      f_token = create_f_token(f_token_spec, request_id)
      headers = ios_headers() ++ mfa_headers(f_token, request_token) ++ content_header()

      Req.post(@url <> "/login/mfa/request", json: body, headers: headers)
    end

    def mfa_auth(response) do
      [f_token_spec] = Req.Response.get_header(response, "f-token-spec")
      [request_token] = Req.Response.get_header(response, "request-token")
      [request_id] = Req.Response.get_header(response, "f-request-id")

      body = %{code: "001337"}

      f_token = create_f_token(f_token_spec, request_id)
      headers = ios_headers() ++ mfa_headers(f_token, request_token) ++ content_header()

      Req.post(@url <> "/login/mfa", json: body, headers: headers)
    end

    def account(resource_type, body, response) do
      [request_token] = Req.Response.get_header(response, "request-token")
      [f_token_spec] = Req.Response.get_header(response, "f-token-spec")
      [request_id] = Req.Response.get_header(response, "f-request-id")

      f_token = create_f_token(f_token_spec, request_id)
      account_id = List.first(body["accounts"]["checking"])["id"]

      headers = ios_headers() ++ mfa_headers(f_token, request_token)

      resource =
        case resource_type do
          :balance -> "/accounts/#{account_id}/balances"
          :details -> "/accounts/#{account_id}/details"
        end

      Req.get(@url <> resource, headers: headers)
    end

    defp ios_headers do
      [
        user_agent: "Teller Bank iOS 1.2",
        api_key: @api_key,
        device_id: @device_id,
        accept: "application/json"
      ]
    end

    defp mfa_headers(f_token, request_token) do
      [
        "teller-is-hiring": "I know!",
        "request-token": request_token,
        "f-token": f_token
      ]
    end

    defp content_header, do: [content_type: "application/json"]

    defp create_f_token(spec, req_id) do
      # Used https://www.dcode.fr/cipher-identifier to identify the cipher
      # as the Atbash substitution cipher
      spec = spec |> Base.decode64!(padding: false) |> Jason.decode!()

      message =
        spec["values"]
        |> Enum.map(&spec_value(&1, req_id))
        |> Enum.join(spec["separator"])

      :crypto.hash(:sha3_512, message) |> Base.encode32(case: :lower, padding: false)
    end

    defp spec_value(value, req_id) do
      case value do
        "device-id" -> @device_id
        "last-request-id" -> req_id
        "username" -> System.get_env("username")
        "api-key" -> @api_key
      end
    end
  end

  def decrypt_account(session_key, enc_number) do
    info = session_key |> Base.decode64!() |> Jason.decode!()
    key = info["key"] |> Base.decode64!()
    number = Base.decode64!(enc_number)

    <<_::binary-16, pad::binary>> =
      :crypto.crypto_one_time(:aes_128_ecb, key, number, false)

    :binary.part(pad, 0, byte_size(pad) - :binary.last(pad))
  end
end

username = Kino.Input.read(username)
System.put_env("username", username)
password = Kino.Input.read(password)

TellerBank.Client.fetch(username, password)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment