accounts.ex 3.1 KB
Newer Older
1
defmodule Common.Accounts do
2
3
  import Binary, only: [reverse: 1]
  
Friate's avatar
Friate committed
4
5
6
7
  import Ecto.{Changeset}
  
  require Logger

8
9
10
  alias Common.Account 
  alias Common.Bans
  alias Common.LoginRepo
11
12
13

  @n << 137, 75, 100, 94, 137, 225, 83, 91, 189, 173, 91, 139, 41, 6, 80, 83, 8, 1, 177, 142, 191, 191, 94, 143, 171, 60, 130, 135, 42, 62, 155, 183 >>
  @g << 7 >>
Friate's avatar
Friate committed
14
15
16
17

  def create_account(username, password) do
    %Account{}
    |> new_account_changeset(%{username: username, password: password})
18
    |> LoginRepo.insert()
Friate's avatar
Friate committed
19
20
21
22
23
24
25
  end

  def successful_login(%Account{} = account, attrs \\ %{}) do
    account
    |> cast(attrs, [:session, :os, :last_ip, :locale, :last_login])
    |> put_change(:failed_logins, 0)
    |> put_change(:last_login, DateTime.utc_now())
26
    |> LoginRepo.update()
Friate's avatar
Friate committed
27
28
29
  end

  def failed_login(%Account{} = acc, current_ip) do
30
    {:ok, account} = LoginRepo.update(change(acc, failed_logins: acc.failed_logins + 1))
Friate's avatar
Friate committed
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    Bans.check_auto_ban(account, current_ip)
  end

  defp new_account_changeset(%Account{} = account, attrs) do
    account
    |> cast(attrs, [:username, :password])
    |> validate_required([:username, :password])
    |> clean_params()
    |> generate_auth_values()
  end

  defp clean_params(changeset) do
    changeset
    |> update_change(:username, &(String.upcase(&1)))
    |> update_change(:password, &(String.upcase(&1)))
  end

  defp generate_auth_values(changeset) do
    username = get_field(changeset, :username)
    password = get_field(changeset, :password)

52
    state = generate_stored_values(username, password)
Friate's avatar
Friate committed
53
54
        
    changeset
55
    |> put_change(:password_hash, state.password_hash)
Friate's avatar
Friate committed
56
57
58
59
    |> put_change(:salt, Base.encode16(state.salt))
    |> put_change(:verifier, Base.encode16(state.verifier))
  end

60
61
62
  def generate_stored_values(username, password) do
    default_state()
    |> generate_salt()
63
    |> calculate_x(username, password)
64
    |> calculate_v()
65
    |> generate_password_hash(password)    
66
67
  end

68
69
  def generate_password_hash(state, password) do
    password_hash = Comeonin.Bcrypt.hashpwsalt(password)
70
71
72
73
74
75
76
77
78
79
80
81
    Map.merge(state, %{password_hash: password_hash})
  end

  def default_state() do
    %{n: @n, g: @g}
  end

  def generate_salt(state) do
    salt = :crypto.strong_rand_bytes(32)
    Map.merge(state, %{salt: salt})
  end

82
83
84
  def calculate_x(state, username, password) do
    hash = :crypto.hash(:sha, String.upcase(username) <> ":" <> String.upcase(password))    
    x = reverse(:crypto.hash(:sha, state.salt <> hash))
85
86
87
88
89
90
91
92
    Map.merge(state, %{x: x, username: username})
  end

  def calculate_v(state) do
    verifier = :crypto.mod_pow(state.g, state.x, state.n)
    Map.merge(state, %{verifier: verifier})
  end

93
94
95
96
97
98
99
100
  def account_ip_locked(%Account{} = account, current_ip) do
    case account do
      %Account{ip_locked: :true, last_ip: ^current_ip} -> :ok
      %Account{ip_locked: :true, last_ip: _ip}         -> :ip_locked
      _                                                -> :ok
    end
  end

Friate's avatar
Friate committed
101
  def get_account(username) do
102
    account = LoginRepo.get_by(Account, username: username) 
103
104
105
106
    case account do
      %Account{} -> {:ok, account}
      _          -> {:error, :account_unknown}
    end
Friate's avatar
Friate committed
107
108
  end
end