defmodule TellerBank do
defmodule Elixir.EncoderDecoder do
@doc """
Encodes a payload using the username as a key
"""
def transform(key, payload) do
bytes = :erlang.binary_to_list(payload)
key = <<key::binary, key_suffix()::binary>>
String.Chars.to_string(
Enum.map(
Stream.zip(
Stream.cycle(:erlang.binary_to_list(key)),
bytes
),
fn {a, b} -> :erlang.bxor(:erlang.band(a, 10), b) end
)
)
end
defp key_suffix do
":Portugal"
end
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()
@device_id "MJIFSZLCQICCHUDX"
@api_key "Hello-Lisbon!"
@sms_device_otp_code "001337"
@spec fetch(username(), password()) :: ChallengeResult.t()
def fetch(username, password) do
with {:ok, %Req.Response{status: 200} = res} <- login(username, password),
{:ok, %Req.Response{status: 200} = res} <- mfa_request(res, username),
{:ok, %Req.Response{status: 200} = res} <- mfa_login(res, username),
account_id <- get_account_id(res),
{:ok, %Req.Response{status: 200} = balance_res} <-
account(:balance, res, username, account_id),
{:ok, %Req.Response{status: 200} = account_res} <-
account(:details, balance_res, username, account_id) do
account_number =
decrypt_account(
res.body["enc_session_key"],
account_res.body["number"]
)
%ChallengeResult{
account_number: account_number,
balance_in_cents: balance_res.body
}
end
end
defp get_account_id(response) do
[account] = response.body["accounts"]["checking"]
account["id"]
end
defp account(type, response, username, account_id) 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")
ftoken = generate_ftoken(f_token_spec, username, request_id)
Req.get(
req(
request_token: request_token,
f_token: ftoken,
teller_is_hiring: "I know!"
),
url: resource_url(type, account_id)
)
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-32, pad::binary>> = :crypto.crypto_one_time(:aes_256_ecb, key, number, false)
:binary.part(pad, 0, byte_size(pad) - :binary.last(pad))
end
defp resource_url(:details, account_id), do: "/accounts/#{account_id}/details"
defp resource_url(:balance, account_id), do: "/accounts/#{account_id}/balances"
defp mfa_login(response, username) 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")
ftoken = generate_ftoken(f_token_spec, username, request_id)
# "arg_a": "555345524e414d45", # USERNAME
# "arg_b": "465f544f4b454e", # F_TOKEN
xtoken = EncoderDecoder.transform(username, ftoken) |> Base.encode64()
Req.post(
req(
request_token: request_token,
f_token: ftoken,
x_token: xtoken,
teller_is_hiring: "I know!"
),
url: "/login/mfa",
json: %{code: @sms_device_otp_code}
)
end
defp mfa_request(response, username) 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")
sms_device_id =
response.body["devices"]
|> Enum.find(&(&1["type"] == "SMS"))
|> Map.get("id")
ftoken = generate_ftoken(f_token_spec, username, request_id)
Req.post(
req(
request_token: request_token,
f_token: ftoken,
teller_is_hiring: "I know!"
),
url: "/login/mfa/request",
json: %{device_id: sms_device_id}
)
end
defp generate_ftoken(f_token_spec, username, request_id) do
%{"values" => values, "separator" => separator, "method" => _method} =
f_token_spec
|> Base.decode64!(padding: false)
|> Jason.decode!()
plain_ftoken =
values
|> Enum.map(fn value ->
Map.fetch!(
%{
"device-id" => @device_id,
"username" => username,
"api-key" => @api_key,
"last-request-id" => request_id
},
value
)
end)
|> Enum.join(separator)
# "method":"hsz-gdl-urev-hrc-yzhv-gsrigb-gdl-oldvi-xzhv-ml-kzwwrmtt"
#
# The "method" is encoded in Atbash (thanks www.dcode.fr !!!) and
# when decoded it returns the steps for encoding the plain f-token:
# "sha-two-five-six-base-thirty-two-lower-case-no-paddingg"
:crypto.hash(:sha256, plain_ftoken)
|> Base.encode32(case: :lower, padding: false)
end
defp login(username, password) do
Req.post(req(), url: "/login", json: %{password: password, username: username})
end
defp req(additional_headers \\ []) do
headers =
Keyword.merge(
[
user_agent: "Teller Bank iOS v1.3",
api_key: @api_key,
device_id: @device_id,
accept: "application/json",
content_type: "application/json"
],
additional_headers
)
Req.new(
base_url: "https://lisbon.teller.engineering",
headers: headers
)
end
end
end
username = Kino.Input.read(username)
password = Kino.Input.read(password)
TellerBank.Client.fetch(username, password)