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
@url "https://challenge.teller.engineering/"
@device_id "E7XRJC6WWGMXSZAE"
@api_key "HelloMountainView!"
@type username() :: String.t()
@type password() :: String.t()
@spec fetch(username, password) :: ChallengeResult.t()
def fetch(username, password) do
with {:ok, %Req.Response{body: body} = res} <- Client.login(username, password),
{:ok, res} <- Client.mfa_request(body, res),
{:ok, %Req.Response{body: body} = res} <- Client.mfa_auth(res),
{:ok, %Req.Response{body: balances} = res} <- Client.account(:balance, body, res),
{:ok, %Req.Response{body: acc}} <- Client.account(:details, body, res) do
%ChallengeResult{
account_number: TellerBank.decrypt_account(body["enc_session_key"], acc["number"]),
balance_in_cents: balances
}
end
end
def login(username, password) do
body = %{username: username, password: password}
headers = ios_headers() ++ content_header()
Req.post(@url <> "login", json: body, headers: headers)
end
def mfa_request(body, response) do
[f_token_spec] = Req.Response.get_header(response, "f-token-spec")
[request_token] = Req.Response.get_header(response, "request-token")
[request_id] = Req.Response.get_header(response, "f-request-id")
body = %{device_id: List.last(body["devices"])["id"]}
f_token = create_f_token(f_token_spec, request_id)
headers = ios_headers() ++ mfa_headers(f_token, request_token) ++ content_header()
Req.post(@url <> "/login/mfa/request", json: body, headers: headers)
end
def mfa_auth(response) do
[f_token_spec] = Req.Response.get_header(response, "f-token-spec")
[request_token] = Req.Response.get_header(response, "request-token")
[request_id] = Req.Response.get_header(response, "f-request-id")
body = %{code: "001337"}
f_token = create_f_token(f_token_spec, request_id)
headers = ios_headers() ++ mfa_headers(f_token, request_token) ++ content_header()
Req.post(@url <> "/login/mfa", json: body, headers: headers)
end
def account(resource_type, body, response) do
[request_token] = Req.Response.get_header(response, "request-token")
[f_token_spec] = Req.Response.get_header(response, "f-token-spec")
[request_id] = Req.Response.get_header(response, "f-request-id")
f_token = create_f_token(f_token_spec, request_id)
account_id = List.first(body["accounts"]["checking"])["id"]
headers = ios_headers() ++ mfa_headers(f_token, request_token)
resource =
case resource_type do
:balance -> "/accounts/#{account_id}/balances"
:details -> "/accounts/#{account_id}/details"
end
Req.get(@url <> resource, headers: headers)
end
defp ios_headers do
[
user_agent: "Teller Bank iOS 1.2",
api_key: @api_key,
device_id: @device_id,
accept: "application/json"
]
end
defp mfa_headers(f_token, request_token) do
[
"teller-is-hiring": "I know!",
"request-token": request_token,
"f-token": f_token
]
end
defp content_header, do: [content_type: "application/json"]
defp create_f_token(spec, req_id) do
# Used https://www.dcode.fr/cipher-identifier to identify the cipher
# as the Atbash substitution cipher
spec = spec |> Base.decode64!(padding: false) |> Jason.decode!()
message =
spec["values"]
|> Enum.map(&spec_value(&1, req_id))
|> Enum.join(spec["separator"])
:crypto.hash(:sha3_512, message) |> Base.encode32(case: :lower, padding: false)
end
defp spec_value(value, req_id) do
case value do
"device-id" -> @device_id
"last-request-id" -> req_id
"username" -> System.get_env("username")
"api-key" -> @api_key
end
end
end
def decrypt_account(session_key, enc_number) do
info = session_key |> Base.decode64!() |> Jason.decode!()
key = info["key"] |> Base.decode64!()
number = Base.decode64!(enc_number)
<<_::binary-16, pad::binary>> =
:crypto.crypto_one_time(:aes_128_ecb, key, number, false)
:binary.part(pad, 0, byte_size(pad) - :binary.last(pad))
end
end
username = Kino.Input.read(username)
System.put_env("username", username)
password = Kino.Input.read(password)
TellerBank.Client.fetch(username, password)