Teller Bank Challenge
Mix.install([:req, :jason, :kino])
Your Solution
username = Kino.Input.text("Username") |> Kino.render()
password = Kino.Input.text("Password")
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()
@base_url "https://challenge.teller.engineering"
@api_key "HelloMountainView!"
@device_id "XAY6ZC6RACIIXXWY"
@default_headers [
{"user-agent", "Teller Bank iOS 1.2"},
{"api-key", @api_key},
{"device-id", @device_id},
{"content-type", "application/json"},
{"accept", "application/json"}
]
@spec fetch(username, password) :: ChallengeResult.t()
def fetch(username, password) do
login_response = login(username, password)
mfa_response = mfa_request(login_response, username)
auth_response = mfa_authenticate(mfa_response, username)
account_id = get_account_id(auth_response)
balance_response = get_balances(auth_response, account_id, username)
balance_in_cents = get_account_balance(balance_response)
details_response = get_account_details(balance_response, account_id, username)
enc_session_key = get_session_key(auth_response)
account_number = get_account_number(details_response, enc_session_key)
%ChallengeResult{account_number: account_number, balance_in_cents: balance_in_cents}
end
defp login(username, password) do
url = @base_url <> "/login"
body = %{password: password, username: username}
request = Req.new()
Req.post!(request, url: url, headers: @default_headers, json: body)
end
defp mfa_request(response, username) do
url = @base_url <> "/login/mfa/request"
request_headers = generate_headers(response.headers, username)
device_id =
case response.body do
%{
"devices" => [
%{
"id" => device_id,
"mask" => _mask,
"type" => "SMS"
},
_
]
} ->
device_id
%{
"devices" => [
_,
%{
"id" => device_id,
"mask" => _mask,
"type" => "SMS"
}
]
} ->
device_id
end
body = %{device_id: device_id}
# IO.inspect(request_headers, label: "headers")
# IO.inspect(body, label: "body")
request = Req.new()
Req.post!(request, url: url, headers: request_headers, json: body)
end
defp mfa_authenticate(response, username) do
url = @base_url <> "/login/mfa"
request_headers = generate_headers(response.headers, username)
body = %{code: "001337"}
# IO.inspect(request_headers, label: "headers")
# IO.inspect(body, label: "body")
request = Req.new()
Req.post!(request, url: url, headers: request_headers, json: body)
end
defp get_account_id(response) do
%{
"accounts" => %{
"checking" => [
%{
"id" => account_id
}
]
}
} = response.body
account_id
end
defp get_balances(response, account_id, username) do
url = @base_url <> "/accounts/#{account_id}/balances"
request_headers = generate_headers(response.headers, username)
request = Req.new()
Req.get!(request, url: url, headers: request_headers)
end
defp get_account_balance(response) do
balance_available = Map.get(response.body, "available", 0)
# IO.inspect(response, label: "response")
# IO.inspect(balance_available, label: "balance")
balance_available
end
defp get_account_details(response, account_id, username) do
url = @base_url <> "/accounts/#{account_id}/details"
request_headers = generate_headers(response.headers, username)
request = Req.new()
Req.get!(request, url: url, headers: request_headers)
end
defp get_session_key(response) do
session_key = Map.get(response.body, "enc_session_key", "")
# IO.inspect(response, label: "response")
# IO.inspect(session_key, label: "session_key")
# Decoded as %{"cipher" => "128-ECB", "key" => "zJB0TRan07Pc/kBy8XIPiA=="}
session_key
|> Base.decode64!()
|> Jason.decode!()
# |> IO.inspect(label: "decoded_session_key")
|> Map.fetch!("key")
|> Base.decode64!()
end
defp get_account_number(response, key) do
encoded_number = Map.get(response.body, "number", "")
{:ok, ciphertext} = Base.decode64(encoded_number)
<> = ciphertext
decrypted = :crypto.crypto_one_time(:aes_128_ecb, key, iv, ciphertext, false)
last = :binary.last(decrypted)
:binary.part(decrypted, 0, byte_size(decrypted) - last)
end
defp generate_headers(headers, username) do
response_headers = Map.new(headers)
last_request_id = Map.get(response_headers, "f-request-id", "")
request_token = Map.get(response_headers, "request-token", "")
f_token_spec = Map.get(response_headers, "f-token-spec", "")
f_token = f_token_spec |> generate_token(username, last_request_id)
[
{"teller-is-hiring", "I know!"},
{"request-token", request_token},
{"f-token", f_token}
] ++ @default_headers
end
defp generate_token(spec, username, last_request_id) do
%{"method" => _method, "separator" => separator, "values" => values} =
spec |> Base.decode64!(padding: false) |> Jason.decode!()
# spec |> Base.decode64!(padding: false) |> Jason.decode!()
# |> IO.inspect(label: "spec")
# IO.inspect(method, label: "method")
# Using https://www.dcode.fr/cipher-identifier to identify the cypher
# sha-three-five-one-two-base-thirty-two-lower-case-no-padding
# IO.inspect(separator, label: "separator")
# IO.inspect(values, label: "values")
encoded =
Enum.map(values, fn
"last-request-id" -> last_request_id
"username" -> username
"device-id" -> @device_id
"api-key" -> @api_key
end)
|> Enum.join(separator)
# IO.inspect(encoded, label: "encoded")
:sha3_512
|> :crypto.hash(encoded)
|> Base.encode32(case: :lower, padding: false)
end
end
end
username = Kino.Input.read(username)
password = Kino.Input.read(password)
TellerBank.Client.fetch(username, password)