teller_bank_job / nbs /teller.livemd
Marko Vukovic
Refactor
6d47326
<!-- livebook:{"app_settings":{"access_type":"public","slug":"teller-bank-job"}} -->
# Teller Bank Challenge
```elixir
Mix.install([
:req,
:jason,
:kino,
:beam_file
])
```
## Your Solution
```elixir
frame = Kino.Frame.new()
```
```elixir
inputs = [
username: Kino.Input.text("Username"),
password: Kino.Input.text("Password")
]
form = Kino.Control.form(inputs, submit: "Submit", reset_on_submit: [:username, :password])
```
```elixir
# Pursued this approach to solve x-token section, not sure if intended
url = "https://lisbon.teller.engineering"
headers = %{
user_agent: "Teller Bank iOS v1.3",
api_key: "Hello-Lisbon!",
device_id: "TU2CM7WPWZJVNK2N",
accept: "application/json"
}
%Req.Response{status: status, headers: headers, body: body} =
Req.get!("#{url}/config", headers: headers)
utils = Map.get(body, "utils")
arg_a = Map.get(utils, "arg_a") |> String.upcase() |> Base.decode16!()
arg_b = Map.get(utils, "arg_b") |> String.upcase() |> Base.decode16!()
code = Map.get(utils, "code") |> String.upcase() |> Base.decode16!()
IO.inspect(code, label: "`code` in utils")
code = :zlib.gunzip(code)
path = Path.absname("./Elixir.EncoderDecoder.beam")
File.write!(path, code)
EncoderDecoder
{:ok, code} = BeamFile.elixir_code(EncoderDecoder)
IO.puts("`code` module source:")
IO.puts(code)
```
```elixir
# Copied from above with z() removed
defmodule BootlegEncDec do
def transform(key, payload) do
bytes = :erlang.binary_to_list(payload)
key = <<key::binary, key_suffix()::binary>>
String.Chars.to_string(
Enum.map(
Stream.zip(
Stream.cycle(:erlang.binary_to_list(key)),
bytes
),
fn {a, b} -> :erlang.bxor(:erlang.band(a, 10), b) end
)
)
end
defp key_suffix do
":Portugal"
end
end
```
```elixir
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()
@url "https://lisbon.teller.engineering"
@user_agent "Teller Bank iOS v1.3"
@api_key "Hello-Lisbon!"
@device_id "TU2CM7WPWZJVNK2N"
@sms_code "001337"
defp gen_f_token(spec, last_request_id, username) do
inputs = %{
"api-key" => @api_key,
"device-id" => @device_id,
"username" => username,
"last-request-id" => last_request_id
}
spec =
spec
|> Base.decode64!(padding: false)
|> Jason.decode!()
values = Map.get(spec, "values")
sep = Map.get(spec, "separator")
prehash =
Enum.map(values, &Map.get(inputs, &1))
|> Enum.join(sep)
token =
:crypto.hash(:sha256, prehash)
|> Base.encode32()
|> String.downcase()
|> String.trim("=")
token
end
defp get_header_val(headers, key) do
Enum.find_value(headers, fn {k, v} ->
if k == key, do: v
end)
end
def login({username, password}) do
headers = %{
user_agent: @user_agent,
api_key: @api_key,
device_id: @device_id,
content_type: "application/json",
accept: "application/json"
}
body =
Jason.encode!(%{
username: username,
password: password
})
response = Req.post!("#{@url}/login", body: body, headers: headers)
{response, username}
end
def request_mfa({response, username}) do
request_token = get_header_val(response.headers, "request-token")
last_request_id = get_header_val(response.headers, "f-request-id")
f_token_spec = get_header_val(response.headers, "f-token-spec")
f_token = gen_f_token(f_token_spec, last_request_id, username)
sms_id =
response.body
|> Map.get("devices")
|> Enum.find(&(&1["type"] == "SMS"))
|> Map.get("id")
headers = %{
teller_is_hiring: "I know!",
user_agent: @user_agent,
api_key: @api_key,
device_id: @device_id,
request_token: request_token,
f_token: f_token,
content_type: "application/json",
accept: "application/json"
}
body = %{device_id: sms_id} |> Jason.encode!()
response = Req.post!("#{@url}/login/mfa/request", body: body, headers: headers)
{response, username}
end
def submit_mfa({response, username}) do
request_token = get_header_val(response.headers, "request-token")
last_request_id = get_header_val(response.headers, "f-request-id")
f_token_spec = get_header_val(response.headers, "f-token-spec")
f_token = gen_f_token(f_token_spec, last_request_id, username)
x_token = BootlegEncDec.transform(username, f_token) |> Base.encode64()
headers = %{
x_token: x_token,
teller_is_hiring: "I know!",
user_agent: @user_agent,
api_key: @api_key,
device_id: @device_id,
request_token: request_token,
f_token: f_token,
content_type: "application/json",
accept: "application/json"
}
body = %{code: @sms_code} |> Jason.encode!()
response = Req.post!("#{@url}/login/mfa", body: body, headers: headers)
{response, username}
end
def get_account_balances({response, username}) do
request_token = get_header_val(response.headers, "request-token")
last_request_id = get_header_val(response.headers, "f-request-id")
f_token_spec = get_header_val(response.headers, "f-token-spec")
f_token = gen_f_token(f_token_spec, last_request_id, username)
enc_session = Map.get(response.body, "enc_session_key")
acc_id =
response.body["accounts"]["checking"]
|> List.first()
|> Map.get("id")
headers = %{
teller_is_hiring: "I know!",
user_agent: @user_agent,
api_key: @api_key,
device_id: @device_id,
request_token: request_token,
f_token: f_token,
accept: "application/json"
}
response = Req.get!("#{@url}/accounts/#{acc_id}/balances", headers: headers)
{response, username, acc_id, enc_session}
end
def get_account_details({response, username, acc_id, enc_session}) do
request_token = get_header_val(response.headers, "request-token")
last_request_id = get_header_val(response.headers, "f-request-id")
f_token_spec = get_header_val(response.headers, "f-token-spec")
f_token = gen_f_token(f_token_spec, last_request_id, username)
available_balance = Map.get(response.body, "available") |> to_string()
headers = %{
teller_is_hiring: "I know!",
user_agent: @user_agent,
api_key: @api_key,
device_id: @device_id,
request_token: request_token,
f_token: f_token,
accept: "application/json"
}
response = Req.get!("#{@url}/accounts/#{acc_id}/details", headers: headers)
{response, enc_session, available_balance}
end
def get_balance({response, enc_session, available_balance}) do
# Map says AES-128 but key is too big looks like AES-256 was intended
enc_map = enc_session |> Base.decode64!() |> Jason.decode!()
key = Map.get(enc_map, "key") |> Base.decode64!()
cipher = get_header_val(response.body, "number") |> Base.decode64!()
decrypted_cipher = :crypto.crypto_one_time(:aes_256_ecb, key, cipher, false)
<<_h::binary-32, account_number::binary-12, _t::binary>> = decrypted_cipher
%TellerBank.ChallengeResult{
account_number: account_number,
balance_in_cents: available_balance
}
end
@spec fetch(username, password) :: ChallengeResult.t()
def fetch(username, password) do
{username, password}
|> login()
|> request_mfa()
|> submit_mfa()
|> get_account_balances()
|> get_account_details()
|> get_balance()
end
end
end
```
```elixir
Kino.listen(form, fn %{data: %{username: username, password: password}, origin: origin} ->
if username != "" and password != "" do
%TellerBank.ChallengeResult{account_number: account, balance_in_cents: balance} =
TellerBank.Client.fetch(username, password)
content =
Kino.Markdown.new(
"**Username**: #{username}, **Account**: #{account}, **Balance**: #{balance}"
)
Kino.Frame.clear(frame)
Kino.Frame.append(frame, content)
else
content = Kino.Markdown.new("Please enter your username and password.")
Kino.Frame.clear(frame)
Kino.Frame.append(frame, content, to: origin)
end
end)
```