Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us
Notesclub

Teller Bank Challenge

teller-challenge.livemd

Teller Bank Challenge

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

Your 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
    @type username() :: String.t()
    @type password() :: String.t()

    @base_url "https://challenge.teller.engineering"
    @api_key "HelloMountainView!"
    @device_id "XAY6ZC6RACIIXXWY"
    @default_headers [
      {"user-agent", "Teller Bank iOS 1.2"},
      {"api-key", @api_key},
      {"device-id", @device_id},
      {"content-type", "application/json"},
      {"accept", "application/json"}
    ]

    @spec fetch(username, password) :: ChallengeResult.t()
    def fetch(username, password) do
      login_response = login(username, password)
      mfa_response = mfa_request(login_response, username)
      auth_response = mfa_authenticate(mfa_response, username)

      account_id = get_account_id(auth_response)
      balance_response = get_balances(auth_response, account_id, username)
      balance_in_cents = get_account_balance(balance_response)
      details_response = get_account_details(balance_response, account_id, username)

      enc_session_key = get_session_key(auth_response)
      account_number = get_account_number(details_response, enc_session_key)

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

    defp login(username, password) do
      url = @base_url <> "/login"
      body = %{password: password, username: username}
      request = Req.new()
      Req.post!(request, url: url, headers: @default_headers, json: body)
    end

    defp mfa_request(response, username) do
      url = @base_url <> "/login/mfa/request"
      request_headers = generate_headers(response.headers, username)

      device_id =
        case response.body do
          %{
            "devices" => [
              %{
                "id" => device_id,
                "mask" => _mask,
                "type" => "SMS"
              },
              _
            ]
          } ->
            device_id

          %{
            "devices" => [
              _,
              %{
                "id" => device_id,
                "mask" => _mask,
                "type" => "SMS"
              }
            ]
          } ->
            device_id
        end

      body = %{device_id: device_id}
      # IO.inspect(request_headers, label: "headers")
      # IO.inspect(body, label: "body")
      request = Req.new()
      Req.post!(request, url: url, headers: request_headers, json: body)
    end

    defp mfa_authenticate(response, username) do
      url = @base_url <> "/login/mfa"
      request_headers = generate_headers(response.headers, username)
      body = %{code: "001337"}
      # IO.inspect(request_headers, label: "headers")
      # IO.inspect(body, label: "body")
      request = Req.new()
      Req.post!(request, url: url, headers: request_headers, json: body)
    end

    defp get_account_id(response) do
      %{
        "accounts" => %{
          "checking" => [
            %{
              "id" => account_id
            }
          ]
        }
      } = response.body

      account_id
    end

    defp get_balances(response, account_id, username) do
      url = @base_url <> "/accounts/#{account_id}/balances"
      request_headers = generate_headers(response.headers, username)
      request = Req.new()
      Req.get!(request, url: url, headers: request_headers)
    end

    defp get_account_balance(response) do
      balance_available = Map.get(response.body, "available", 0)
      # IO.inspect(response, label: "response")
      # IO.inspect(balance_available, label: "balance")
      balance_available
    end

    defp get_account_details(response, account_id, username) do
      url = @base_url <> "/accounts/#{account_id}/details"
      request_headers = generate_headers(response.headers, username)
      request = Req.new()
      Req.get!(request, url: url, headers: request_headers)
    end

    defp get_session_key(response) do
      session_key = Map.get(response.body, "enc_session_key", "")
      # IO.inspect(response, label: "response")
      # IO.inspect(session_key, label: "session_key")
      # Decoded as %{"cipher" => "128-ECB", "key" => "zJB0TRan07Pc/kBy8XIPiA=="}
      session_key
      |> Base.decode64!()
      |> Jason.decode!()
      # |> IO.inspect(label: "decoded_session_key")
      |> Map.fetch!("key")
      |> Base.decode64!()
    end

    defp get_account_number(response, key) do
      encoded_number = Map.get(response.body, "number", "")
      {:ok, ciphertext} = Base.decode64(encoded_number)
      <> = ciphertext
      decrypted = :crypto.crypto_one_time(:aes_128_ecb, key, iv, ciphertext, false)
      last = :binary.last(decrypted)
      :binary.part(decrypted, 0, byte_size(decrypted) - last)
    end

    defp generate_headers(headers, username) do
      response_headers = Map.new(headers)
      last_request_id = Map.get(response_headers, "f-request-id", "")
      request_token = Map.get(response_headers, "request-token", "")
      f_token_spec = Map.get(response_headers, "f-token-spec", "")
      f_token = f_token_spec |> generate_token(username, last_request_id)

      [
        {"teller-is-hiring", "I know!"},
        {"request-token", request_token},
        {"f-token", f_token}
      ] ++ @default_headers
    end

    defp generate_token(spec, username, last_request_id) do
      %{"method" => _method, "separator" => separator, "values" => values} =
        spec |> Base.decode64!(padding: false) |> Jason.decode!()

      # spec |> Base.decode64!(padding: false) |> Jason.decode!()
      # |> IO.inspect(label: "spec")
      # IO.inspect(method, label: "method")
      # Using https://www.dcode.fr/cipher-identifier to identify the cypher
      # sha-three-five-one-two-base-thirty-two-lower-case-no-padding
      # IO.inspect(separator, label: "separator")
      # IO.inspect(values, label: "values")
      encoded =
        Enum.map(values, fn
          "last-request-id" -> last_request_id
          "username" -> username
          "device-id" -> @device_id
          "api-key" -> @api_key
        end)
        |> Enum.join(separator)

      # IO.inspect(encoded, label: "encoded")

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

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

TellerBank.Client.fetch(username, password)