aboutsummaryrefslogtreecommitdiff
path: root/test/ejabberd_cyrsasl_test.exs
blob: bdef92cd4c27cef2e90b1b6af55107ea8d6a772c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# ----------------------------------------------------------------------
#
# ejabberd, Copyright (C) 2002-2017   ProcessOne
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# ----------------------------------------------------------------------

defmodule EjabberdCyrsaslTest do
  @author "pawel@process-one.net"

  use ExUnit.Case, async: true

  setup_all do
    :ok = :ejabberd.start_app(:lager)
    :p1_sha.load_nif()
    :mnesia.start
    :ejabberd_mnesia.start
    :ok = start_module(:stringprep)
    start_module(:jid)
    :ejabberd_hooks.start_link
    :ok = :ejabberd_config.start(["domain1"], [])
    {:ok, _} = :cyrsasl.start_link
    cyrstate = :cyrsasl.server_new("domain1", "domain1", "domain1", :ok, &get_password/1,
                                   &check_password/3, &check_password_digest/5)
    setup_anonymous_mocks()
    {:ok, cyrstate: cyrstate}
  end

  test "Plain text (correct user and pass)", context do
    step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"pass">>)
    assert {:ok, _} = step1
    {:ok, kv} = step1
    assert kv[:authzid] == "user1", "got correct user"
  end

  test "Plain text (correct user wrong pass)", context do
    step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"user1",0,"badpass">>)
    assert step1 == {:error, :not_authorized, "user1"}
  end

  test "Plain text (wrong user wrong pass)", context do
    step1 = :cyrsasl.server_start(context[:cyrstate], "PLAIN", <<0,"nouser1",0,"badpass">>)
    assert step1 == {:error, :not_authorized, "nouser1"}
  end

  test "Anonymous", context do
    step1 = :cyrsasl.server_start(context[:cyrstate], "ANONYMOUS", "domain1")
    assert {:ok, _} = step1
  end

  test "Digest-MD5 (correct user and pass)", context do
    assert {:ok, _list} = process_digest_md5(context[:cyrstate], "user1", "domain1", "pass")
  end

  test "Digest-MD5 (correct user wrong pass)", context do
    assert {:error, :not_authorized, "user1"} = process_digest_md5(context[:cyrstate], "user1", "domain1", "badpass")
  end

  test "Digest-MD5 (wrong user correct pass)", context do
    assert {:error, :not_authorized, "baduser"} = process_digest_md5(context[:cyrstate], "baduser", "domain1", "pass")
  end

  test "Digest-MD5 (wrong user and pass)", context do
    assert {:error, :not_authorized, "baduser"} = process_digest_md5(context[:cyrstate], "baduser", "domain1", "badpass")
  end

  defp process_digest_md5(cyrstate, user, domain, pass) do
    assert {:continue, init_str, state1} = :cyrsasl.server_start(cyrstate, "DIGEST-MD5", "")
    assert [_, nonce] = Regex.run(~r/nonce="(.*?)"/, init_str)
    digest_uri = "xmpp/#{domain}"
    cnonce = "abcd"
    nc = "00000001"
    response_hash = calc_digest_md5(user, domain, pass, nc, nonce, cnonce)
    response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <>
      "nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <>
      "charset=utf-8,algorithm=md5-sess"
    case :cyrsasl.server_step(state1, response) do
      {:continue, _calc_str, state2} -> :cyrsasl.server_step(state2, "")
      other -> other
    end
  end

  defp calc_digest_md5(user, domain, pass, nc, nonce, cnonce) do
      digest_uri = "xmpp/#{domain}"
      a0 = "#{user}:#{domain}:#{pass}"
      a1 = "#{str_md5(a0)}:#{nonce}:#{cnonce}"
      a2 = "AUTHENTICATE:#{digest_uri}"
      hex_md5("#{hex_md5(a1)}:#{nonce}:#{nc}:#{cnonce}:auth:#{hex_md5(a2)}")
     end

  defp str_md5(str) do
    :erlang.md5(str)
  end

  defp hex_md5(str) do
    :p1_sha.to_hexlist(:erlang.md5(str))
  end

  defp setup_anonymous_mocks() do
    :meck.unload
    mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled,
      fn (_host) ->
        true
      end)
    mock(:ejabberd_auth, :user_exists,
      fn (user, domain) ->
        domain == "domain1" and get_password(user) != {:false, :internal}
      end)
  end

  defp start_module(module) do
    case apply(module, :start, []) do
      :ok -> :ok
      {:error, {:already_started, _}} -> :ok
      other -> other
    end
  end

  defp get_password(user) do
    if user == "user1" or user == "user2" do
      {"pass", :internal}
    else
      {:false, :internal}
    end
  end

  defp check_password(_user, authzid, pass) do
    case get_password(authzid) do
      {^pass, mod} ->
        {true, mod}
      _ ->
        false
    end
  end

  defp check_password_digest(_user, authzid, _pass, digest, digest_gen) do
    case get_password(authzid) do
      {:false, _} ->
        false
      {spass, mod} ->
        v = digest_gen.(spass)
        if v == digest do
          {true, mod}
        else
          false
        end
    end
  end

  defp mock(module, function, fun) do
    try do
      :meck.new(module, [:non_strict])
    catch
      :error, {:already_started, _pid} -> :ok
    end
    :meck.expect(module, function, fun)
  end
end