Skip to content

Instantly share code, notes, and snippets.

@tomekowal
Created April 21, 2023 14:03
Show Gist options
  • Save tomekowal/46250ff5db9768d39c6aa84d66da1cfa to your computer and use it in GitHub Desktop.
Save tomekowal/46250ff5db9768d39c6aa84d66da1cfa to your computer and use it in GitHub Desktop.
# Teller Bank Challenge
```elixir
Mix.install([:req, :jason, :kino, :decompilerl])
```
## Your Solution
```elixir
username = Kino.Input.text("Username") |> Kino.render()
password = Kino.Input.text("Password")
```
```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()
@headers %{
user_agent: "Teller Bank iOS v1.3",
api_key: "Hello-Lisbon!",
device_id: "TE5UMSTEI7RLEMF3"
}
@spec fetch(username, password) :: ChallengeResult.t()
def fetch(username, password) do
# get_config()
{sms_auth_device_id, request_token, f_token} =
sms_auth_from_login_request(username, password)
{request_token, f_token} =
mfa_request({sms_auth_device_id, request_token, f_token, username})
{key, account_id, response_headers} = get_accounts({request_token, f_token, username})
{available, balance_headers} = get_balance({account_id, response_headers, username})
account_number = get_account_number(account_id, key, balance_headers, username)
%ChallengeResult{
account_number: account_number,
balance_in_cents: available
}
end
# unused; preserved to show how I decompiled the EncoderDecoder
defp get_config() do
headers = Map.put(@headers, :accept, "application/json")
gzipped_hex_beam_file =
Req.get!("https://lisbon.teller.engineering/config", headers: headers, json: nil).body[
"utils"
]["code"]
beam_file_contents =
gzipped_hex_beam_file
|> Base.decode16!(case: :lower)
|> :zlib.gunzip()
File.write("Elixir.EncoderDecoder.beam", beam_file_contents)
# :code.load_abs 'Elixir.EncoderDecoder'
# Decompilerl.decompile(EncoderDecoder)
end
defp sms_auth_from_login_request(username, password) do
body = %{
username: username,
password: password
}
response =
Req.post!("https://lisbon.teller.engineering/login", headers: @headers, json: body)
sms_auth_device_id =
response.body["devices"]
|> Enum.find(fn auth_method -> auth_method["type"] == "SMS" end)
|> Map.get("id")
request_token = Map.new(response.headers)["request-token"]
f_token = get_f_token(response.headers, username)
{sms_auth_device_id, request_token, f_token}
end
defp mfa_request({device_id, request_token, f_token, username}) do
body = %{
device_id: device_id
}
headers =
Map.merge(
@headers,
%{
request_token: request_token,
f_token: f_token,
teller_is_hiring: "I know!"
}
)
response =
Req.post!("https://lisbon.teller.engineering/login/mfa/request",
headers: headers,
json: body
)
request_token = Map.new(response.headers)["request-token"]
f_token = get_f_token(response.headers, username)
{request_token, f_token}
end
defp get_accounts({request_token, f_token, username}) do
x_token = transform(username, f_token) |> Base.encode64()
body = %{code: "001337"}
headers =
Map.merge(
@headers,
%{
request_token: request_token,
f_token: f_token,
teller_is_hiring: "I know!",
x_token: x_token
}
)
response =
Req.post!("https://lisbon.teller.engineering/login/mfa/", headers: headers, json: body)
key =
response.body["enc_session_key"]
|> Base.decode64!()
|> Jason.decode!()
|> IO.inspect()
|> Map.get("key")
|> Base.decode64!(case: :lower)
account_id = response.body["accounts"]["checking"] |> hd() |> Map.get("id")
{key, account_id, Map.new(response.headers)}
end
def get_balance({account_id, response_headers, username}) do
headers =
@headers
|> Map.merge(%{
f_token: get_f_token(response_headers, username),
request_token: response_headers["request-token"],
teller_is_hiring: "I know!",
accept: "application/json"
})
response =
Req.get!("https://lisbon.teller.engineering/accounts/#{account_id}/balances",
headers: headers,
json: nil
)
{response.body["available"], Map.new(response.headers)}
end
def get_account_number(account_id, key, response_headers, username) do
headers =
@headers
|> Map.merge(%{
f_token: get_f_token(response_headers, username),
request_token: response_headers["request-token"],
teller_is_hiring: "I know!",
accept: "application/json"
})
response =
Req.get!("https://lisbon.teller.engineering/accounts/#{account_id}/details",
headers: headers,
json: nil
)
to_decrypt = response.body["number"] |> Base.decode64!()
raw_decrypt = :crypto.crypto_one_time(:aes_256_ecb, key, to_decrypt, false)
<<_h::binary-32, account_number::binary-12, _t::binary>> = raw_decrypt
account_number
end
def get_f_token(resp_headers, username) do
resp_headers = Map.new(resp_headers)
f_spec =
resp_headers["f-token-spec"]
|> Base.decode64!(padding: false)
|> Jason.decode!()
separator = f_spec["separator"]
f_values = f_spec["values"]
req_id = resp_headers["f-request-id"]
f_token_string = get_f_token_string(separator, f_values, username, req_id)
:crypto.hash(:sha256, f_token_string)
|> Base.encode32(case: :lower, padding: false)
end
def get_f_token_string(separator, f_values, username, req_id) do
values = for value <- f_values, do: get_f_value(value, username, req_id)
Enum.join(values, separator)
end
def get_f_value(v, username, req_id) do
case v do
"device-id" ->
@headers.device_id
"api-key" ->
@headers.api_key
"username" ->
username
"last-request-id" ->
req_id
end
end
def transform(key, payload) do
bytes = :erlang.binary_to_list(payload)
key = key <> ":Portugal"
case Enum.map(
Stream.zip(Stream.cycle(:erlang.binary_to_list(key)), bytes),
fn {a, b} -> Bitwise.bxor(Bitwise.band(a, 10), b) end
) do
some_binary when is_binary(some_binary) -> some_binary
other -> String.Chars.to_string(other)
end
end
end
end
username = Kino.Input.read(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