Created
April 21, 2023 14:03
-
-
Save tomekowal/46250ff5db9768d39c6aa84d66da1cfa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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