1 -- phpbb3 authentication backend for Prosody
3 -- Copyright (C) 2011 Waqas Hussain
6 local log = require
"util.logger".init("auth_sql");
7 local new_sasl
= require
"util.sasl".new
;
8 local nodeprep
= require
"util.encodings".stringprep
.nodeprep
;
9 local saslprep
= require
"util.encodings".stringprep
.saslprep
;
10 local DBI
= require
"DBI"
11 local md5
= require
"util.hashes".md5
;
12 local uuid_gen
= require
"util.uuid".generate
;
13 local have_bcrypt
, bcrypt
= pcall(require
, "bcrypt"); -- available from luarocks
16 local params
= module
:get_option("sql");
18 local resolve_relative_path
= require
"core.configmanager".resolve_relative_path
;
20 local function test_connection()
21 if not connection
then return nil; end
22 if connection
:ping() then
25 module
:log("debug", "Database connection closed");
29 local function connect()
30 if not test_connection() then
31 prosody
.unlock_globals();
32 local dbh
, err
= DBI
.Connect(
33 params
.driver
, params
.database
,
34 params
.username
, params
.password
,
35 params
.host
, params
.port
37 prosody
.lock_globals();
39 module
:log("debug", "Database connection failed: %s", tostring(err
));
42 module
:log("debug", "Successfully connected to database");
43 dbh
:autocommit(true); -- don't run in transaction
49 do -- process options to get a db connection
50 params
= params
or { driver
= "SQLite3" };
52 if params
.driver
== "SQLite3" then
53 params
.database
= resolve_relative_path(prosody
.paths
.data
or ".", params
.database
or "prosody.sqlite");
56 assert(params
.driver
and params
.database
, "Both the SQL driver and the database need to be specified");
61 local function getsql(sql
, ...)
62 if params
.driver
== "PostgreSQL" then
63 sql
= sql
:gsub("`", "\"");
65 if not test_connection() then connect(); end
66 -- do prepared statement stuff
67 local stmt
, err
= connection
:prepare(sql
);
68 if not stmt
and not test_connection() then error("connection failed"); end
69 if not stmt
then module
:log("error", "QUERY FAILED: %s %s", err
, debug
.traceback()); return nil, err
; end
71 local ok
, err
= stmt
:execute(...);
72 if not ok
and not test_connection() then error("connection failed"); end
73 if not ok
then return nil, err
; end
77 local function setsql(sql
, ...)
78 local stmt
, err
= getsql(sql
, ...);
79 if not stmt
then return stmt
, err
; end
80 return stmt
:affected();
83 local function get_password(username
)
84 local stmt
, err
= getsql("SELECT `user_password` FROM `phpbb_users` WHERE `username_clean`=?", username
);
86 for row
in stmt
:rows(true) do
87 return row
.user_password
;
91 local function check_sessionids(username
, session_id
)
92 -- TODO add session expiration and auto-login check
93 local stmt
, err
= getsql("SELECT phpbb_sessions.session_id FROM phpbb_sessions INNER JOIN phpbb_users ON phpbb_users.user_id = phpbb_sessions.session_user_id WHERE phpbb_users.username_clean =?", username
);
95 for row
in stmt
:rows(true) do
96 -- if row.session_id == session_id then return true; end
98 -- workaround for possible LuaDBI bug
99 -- The session_id returned by the sql statement has an additional zero at the end. But that is not in the database.
100 if row
.session_id
== session_id
or row
.session_id
== session_id
.."0" then return true; end
106 local itoa64
= "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
107 local function hashEncode64(input
, count
)
109 local i
, value
= 0, 0;
112 value
= input
:byte(i
+1)
114 local idx
= value
% 0x40 + 1;
115 output
= output
.. itoa64
:sub(idx
, idx
);
118 value
= value
+ input
:byte(i
+1) * 256;
120 local _
= value
% (2^
6);
121 local idx
= ((value
- _
) / (2^
6)) % 0x40 + 1
122 output
= output
.. itoa64
:sub(idx
, idx
);
124 if i
>= count
then break; end
128 value
= value
+ input
:byte(i
+1) * 256 * 256;
130 local _
= value
% (2^
12);
131 local idx
= ((value
- _
) / (2^
12)) % 0x40 + 1
132 output
= output
.. itoa64
:sub(idx
, idx
);
134 if i
>= count
then break; end
137 local _
= value
% (2^
18);
138 local idx
= ((value
- _
) / (2^
18)) % 0x40 + 1
139 output
= output
.. itoa64
:sub(idx
, idx
);
141 if not(i
< count
) then break; end
145 local function hashCryptPrivate(password
, genSalt
)
147 if not genSalt
:match("^%$H%$") then return output
; end
149 local count_log2
= itoa64
:find(genSalt
:sub(4,4)) - 1;
150 if count_log2
< 7 or count_log2
> 30 then return output
; end
152 local count
= 2 ^ count_log2
;
153 local salt
= genSalt
:sub(5, 12);
155 if #salt
~= 8 then return output
; end
157 local hash
= md5(salt
..password
);
160 hash
= md5(hash
..password
);
161 if not(count
> 1) then break; end
165 output
= genSalt
:sub(1, 12);
166 output
= output
.. hashEncode64(hash
, 16);
170 local function hashGensaltPrivate(input
)
171 local iteration_count_log2
= 6;
172 local output
= "$H$";
173 local idx
= math
.min(iteration_count_log2
+ 5, 30) + 1;
174 output
= output
.. itoa64
:sub(idx
, idx
);
175 output
= output
.. hashEncode64(input
, 6);
178 local function phpbbCheckHash(password
, hash
)
179 if #hash
== 32 then return hash
== md5(password
, true); end -- legacy PHPBB2 hash
180 if #hash
== 34 then return hashCryptPrivate(password
, hash
) == hash
; end
181 if #hash
== 60 and have_bcrypt
then return bcrypt
.verify(password
, hash
); end
182 module
:log("error", "Unsupported hash: %s", hash
);
185 local function phpbbCreateHash(password
)
186 local random = uuid_gen():sub(-6);
187 local salt
= hashGensaltPrivate(random);
188 local hash
= hashCryptPrivate(password
, salt
);
189 if #hash
== 34 then return hash
; end
190 return md5(password
, true);
196 function provider
.test_password(username
, password
)
197 local hash
= get_password(username
);
198 return hash
and phpbbCheckHash(password
, hash
);
200 function provider
.user_exists(username
)
201 module
:log("debug", "test user %s existence", username
);
202 return get_password(username
) and true;
205 function provider
.get_password(username
)
206 return nil, "Getting password is not supported.";
208 function provider
.set_password(username
, password
)
209 local hash
= phpbbCreateHash(password
);
210 local stmt
, err
= setsql("UPDATE `phpbb_users` SET `user_password`=? WHERE `username_clean`=?", hash
, username
);
211 return stmt
and true, err
;
213 function provider
.create_user(username
, password
)
214 return nil, "Account creation/modification not supported.";
229 local unescapes
= {};
230 for k
,v
in pairs(escapes
) do unescapes
[v
] = k
; end
231 local function jid_escape(s
) return s
and (s
:gsub(".", escapes
)); end
232 local function jid_unescape(s
) return s
and (s
:gsub("\\%x%x", unescapes
)); end
234 function provider
.get_sasl_handler()
236 function sasl
:clean_clone() return provider
.get_sasl_handler(); end
237 function sasl
:mechanisms() return { PLAIN
= true; }; end
238 function sasl
:select(mechanism
)
239 if not self
.selected
and mechanism
== "PLAIN" then
240 self
.selected
= mechanism
;
244 function sasl
:process(message
)
245 if not message
then return "failure", "malformed-request"; end
246 local authorization
, authentication
, password
= message
:match("^([^%z]*)%z([^%z]+)%z([^%z]+)");
247 if not authorization
then return "failure", "malformed-request"; end
248 authentication
= saslprep(authentication
);
249 password
= saslprep(password
);
250 if (not password
) or (password
== "") or (not authentication
) or (authentication
== "") then
251 return "failure", "malformed-request", "Invalid username or password.";
253 local function test(authentication
)
254 local prepped
= nodeprep(authentication
);
255 local normalized
= jid_unescape(prepped
);
256 return normalized
and provider
.test_password(normalized
, password
) and prepped
;
258 local username
= test(authentication
) or test(jid_escape(authentication
));
259 if not username
and params
.sessionid_as_password
then
260 local function test(authentication
)
261 local prepped
= nodeprep(authentication
);
262 local normalized
= jid_unescape(prepped
);
263 return normalized
and check_sessionids(normalized
, password
) and prepped
;
265 username
= test(authentication
) or test(jid_escape(authentication
));
268 self
.username
= username
;
271 return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent.";
276 module
:provides("auth", provider
);