Mix.install([:req, :jason, :kino])
Resolving Hex dependencies...
Dependency resolution completed:
New:
castore 0.1.18
finch 0.13.0
hpax 0.1.2
jason 1.3.0
kino 0.6.2
mime 2.0.3
mint 1.4.2
nimble_options 0.4.0
nimble_pool 0.2.6
req 0.3.0
table 0.1.2
telemetry 1.1.0
* Getting req (Hex package)
* Getting jason (Hex package)
* Getting kino (Hex package)
* Getting table (Hex package)
* Getting finch (Hex package)
* Getting mime (Hex package)
* Getting castore (Hex package)
* Getting mint (Hex package)
* Getting nimble_options (Hex package)
* Getting nimble_pool (Hex package)
* Getting telemetry (Hex package)
* Getting hpax (Hex package)
==> nimble_options
Compiling 3 files (.ex)
Generated nimble_options app
==> hpax
Compiling 4 files (.ex)
Generated hpax app
==> nimble_pool
Compiling 2 files (.ex)
Generated nimble_pool app
===> Analyzing applications...
===> Compiling telemetry
==> jason
Compiling 10 files (.ex)
Compiling lib/decoder.ex (it's taking more than 10s)
Generated jason app
==> table
Compiling 5 files (.ex)
Generated table app
==> kino
Compiling 28 files (.ex)
Generated kino app
==> castore
Compiling 1 file (.ex)
Generated castore app
==> mint
Compiling 1 file (.erl)
Compiling 19 files (.ex)
Generated mint app
==> mime
Compiling 1 file (.ex)
Generated mime app
==> finch
Compiling 13 files (.ex)
Generated finch app
==> req
Compiling 5 files (.ex)
Generated req app
:ok
username = Kino.Input.text("Username") |> Kino.render()
password = Kino.Input.text("Password")
defmodule TellerBank do
defmodule OTPCode do
@moduledoc """
You can use this util module to generate your OTP
code dynamically.
"""
@type username() :: String.t()
@spec generate(username) :: String.t()
def generate(username) do
username
|> String.to_charlist()
|> Enum.take(6)
|> Enum.map(&char_to_keypad_number/1)
|> List.to_string()
|> String.pad_leading(6, "0")
end
defp char_to_keypad_number(c) when c in ~c(a b c), do: '2'
defp char_to_keypad_number(c) when c in ~c(d e f), do: '3'
defp char_to_keypad_number(c) when c in ~c(g h i), do: '4'
defp char_to_keypad_number(c) when c in ~c(j k l), do: '5'
defp char_to_keypad_number(c) when c in ~c(m n o), do: '6'
defp char_to_keypad_number(c) when c in ~c(p q r s), do: '7'
defp char_to_keypad_number(c) when c in ~c(t u v), do: '8'
defp char_to_keypad_number(c) when c in ~c(w x y z), do: '9'
defp char_to_keypad_number(_), do: '0'
end
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()
def build_f_token(f_token_spec, username, device_id, api_key, last_request_id) do
encoded =
f_token_spec
|> Base.decode64!()
|> Jason.decode!()
|> Map.get("values")
|> Enum.map(fn
"last-request-id" -> last_request_id
"username" -> username
"device-id" -> device_id
"api-key" -> api_key
end)
|> Enum.join(":")
f_token =
:sha256
|> :crypto.hash(encoded)
|> Base.encode64(padding: false)
end
@spec fetch(username, password) :: ChallengeResult.t()
def fetch(username, password) do
device_id = "CTK7P2CQKSQS4ZER"
api_key = "good-luck-at-the-teller-quiz!"
user_agent = "Teller Bank iOS 1.0"
headers = [
user_agent: user_agent,
api_key: api_key,
device_id: device_id,
accept: "application/json"
]
result =
Req.post!("https://challenge.teller.engineering/login",
headers: headers,
json: %{"username" => username, "password" => password}
)
sms_code =
result
|> Map.get(:body)
|> Map.get("mfa_devices")
|> Enum.find(fn mfa -> Map.get(mfa, "type") == "SMS" end)
|> Map.get("id")
result
result_headers = Map.new(result.headers)
last_request_id = Map.get(result_headers, "f-request-id", "")
f_token =
result_headers
|> Map.get("f-token-spec")
|> build_f_token(username, device_id, api_key, last_request_id)
result =
Req.post!("https://challenge.teller.engineering/login/mfa/request",
headers:
headers ++
[
request_token: Map.get(result_headers, "request-token"),
f_token: f_token,
teller_is_hiring: "I know!"
],
json: %{"device_id" => sms_code}
)
result_headers = Map.new(result.headers)
last_request_id = Map.get(result_headers, "f-request-id", "")
f_token =
result_headers
|> Map.get("f-token-spec")
|> build_f_token(username, device_id, api_key, last_request_id)
result =
Req.post!("https://challenge.teller.engineering/login/mfa",
headers:
headers ++
[
request_token: Map.get(result_headers, "request-token"),
f_token: f_token,
teller_is_hiring: "I know!"
],
json: %{"code" => TellerBank.OTPCode.generate(username)}
)
result_headers = Map.new(result.headers)
last_request_id = Map.get(result_headers, "f-request-id", "")
f_token =
result_headers
|> Map.get("f-token-spec")
|> build_f_token(username, device_id, api_key, last_request_id)
account =
result.body
|> Map.get("accounts")
|> Map.get("checking")
|> Enum.at(0)
|> Map.get("id")
result =
Req.get!("https://challenge.teller.engineering/accounts/#{account}/details",
headers:
headers ++
[
request_token: Map.get(result_headers, "request-token"),
f_token: f_token,
teller_is_hiring: "I know!"
]
)
full_account_id =
result.body
|> Map.get("number")
result_headers = Map.new(result.headers)
last_request_id = Map.get(result_headers, "f-request-id", "")
f_token =
result_headers
|> Map.get("f-token-spec")
|> build_f_token(username, device_id, api_key, last_request_id)
result =
Req.get!("https://challenge.teller.engineering/accounts/#{account}/balances",
headers:
headers ++
[
request_token: Map.get(result_headers, "request-token"),
f_token: f_token,
teller_is_hiring: "I know!"
]
)
amount = Map.get(result.body, "available")
{full_account_id, amount}
%ChallengeResult{account_number: full_account_id, balance_in_cents: amount}
end
end
end
username = Kino.Input.read(username)
password = Kino.Input.read(password)
TellerBank.Client.fetch(username, password)
warning: variable "f_token" is unused (if the variable is not meant to be used, prefix it with an underscore)
#cell:54: TellerBank.Client.build_f_token/5
warning: variable result in code block has no effect as it is never returned (remove the variable or assign it to _ to avoid warnings)
#cell:78: TellerBank.Client.fetch/2
%TellerBank.ChallengeResult{account_number: "865093926084", balance_in_cents: 8259860}