-- Wordpress authentication backend for Prosody
--
-- Copyright (C) 2011 Waqas Hussain
-- Copyright (C) 2011 Kim Alvefur
--
local log = require "util.logger".init("auth_sql");
local new_sasl = require "util.sasl".new;
local nodeprep = require "util.encodings".stringprep.nodeprep;
local saslprep = require "util.encodings".stringprep.saslprep;
local DBI = require "DBI"
local md5 = require "util.hashes".md5;
local uuid_gen = require "util.uuid".generate;
local connection;
local params = module:get_option("sql");
local table_prefix = module:get_option_string("wordpress_table_prefix", "wp_");
local resolve_relative_path = require "core.configmanager".resolve_relative_path;
local function test_connection()
if not connection then return nil; end
if connection:ping() then
return true;
else
module:log("debug", "Database connection closed");
connection = nil;
end
end
local function connect()
if not test_connection() then
prosody.unlock_globals();
local dbh, err = DBI.Connect(
params.driver, params.database,
params.username, params.password,
params.host, params.port
);
prosody.lock_globals();
if not dbh then
module:log("debug", "Database connection failed: %s", tostring(err));
return nil, err;
end
module:log("debug", "Successfully connected to database");
dbh:autocommit(true); -- don't run in transaction
connection = dbh;
return connection;
end
end
do -- process options to get a db connection
params = params or { driver = "SQLite3" };
if params.driver == "SQLite3" then
params.database = resolve_relative_path(prosody.paths.data or ".", params.database or "prosody.sqlite");
end
assert(params.driver and params.database, "Both the SQL driver and the database need to be specified");
assert(connect());
end
local function getsql(sql, ...)
if params.driver == "PostgreSQL" then
sql = sql:gsub("`", "\"");
end
if not test_connection() then connect(); end
-- do prepared statement stuff
local stmt, err = connection:prepare(sql);
if not stmt and not test_connection() then error("connection failed"); end
if not stmt then module:log("error", "QUERY FAILED: %s %s", err, debug.traceback()); return nil, err; end
-- run query
local ok, err = stmt:execute(...);
if not ok and not test_connection() then error("connection failed"); end
if not ok then return nil, err; end
return stmt;
end
local function setsql(sql, ...)
local stmt, err = getsql(sql, ...);
if not stmt then return stmt, err; end
return stmt:affected();
end
local function get_password(username)
local stmt, err = getsql("SELECT `user_pass` FROM `"..table_prefix.."users` WHERE `user_login`=?", username);
if stmt then
for row in stmt:rows(true) do
return row.user_pass;
end
end
end
local itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
local function hashEncode64(input, count)
local output = "";
local i, value = 0, 0;
while true do
value = input:byte(i+1)
i = i+1;
local idx = value % 0x40 + 1;
output = output .. itoa64:sub(idx, idx);
if i < count then
value = value + input:byte(i+1) * 256;
end
local _ = value % (2^6);
local idx = ((value - _) / (2^6)) % 0x40 + 1
output = output .. itoa64:sub(idx, idx);
if i >= count then break; end
i = i+1;
if i < count then
value = value + input:byte(i+1) * 256 * 256;
end
local _ = value % (2^12);
local idx = ((value - _) / (2^12)) % 0x40 + 1
output = output .. itoa64:sub(idx, idx);
if i >= count then break; end
i = i+1;
local _ = value % (2^18);
local idx = ((value - _) / (2^18)) % 0x40 + 1
output = output .. itoa64:sub(idx, idx);
if not(i < count) then break; end
end
return output;
end
local function hashCryptPrivate(password, genSalt)
local output = "*";
if not genSalt:match("^%$P%$") then return output; end
local count_log2 = itoa64:find(genSalt:sub(4,4)) - 1;
if count_log2 < 7 or count_log2 > 30 then return output; end
local count = 2 ^ count_log2;
local salt = genSalt:sub(5, 12);
if #salt ~= 8 then return output; end
local hash = md5(salt..password);
while true do
hash = md5(hash..password);
if not(count > 1) then break; end
count = count-1;
end
output = genSalt:sub(1, 12);
output = output .. hashEncode64(hash, 16);
return output;
end
local function hashGensaltPrivate(input)
local iteration_count_log2 = 6;
local output = "$P$";
local idx = math.min(iteration_count_log2 + 5, 30) + 1;
output = output .. itoa64:sub(idx, idx);
output = output .. hashEncode64(input, 6);
return output;
end
local function wordpressCheckHash(password, hash)
if #hash == 32 then return hash == md5(password, true); end
return #hash == 34 and hashCryptPrivate(password, hash) == hash;
end
local function wordpressCreateHash(password)
local random = uuid_gen():sub(-6);
local salt = hashGensaltPrivate(random);
local hash = hashCryptPrivate(password, salt);
if #hash == 34 then return hash; end
return md5(password, true);
end
provider = {};
function provider.test_password(username, password)
local hash = get_password(username);
return hash and wordpressCheckHash(password, hash);
end
function provider.user_exists(username)
module:log("debug", "test user %s existence", username);
return get_password(username) and true;
end
function provider.get_password(username)
return nil, "Getting password is not supported.";
end
function provider.set_password(username, password)
local hash = wordpressCreateHash(password);
local stmt, err = setsql("UPDATE `"..table_prefix.."users` SET `user_pass`=? WHERE `user_login`=?", hash, username);
return stmt and true, err;
end
function provider.create_user(username, password)
return nil, "Account creation/modification not supported.";
end
local escapes = {
[" "] = "\\20";
['"'] = "\\22";
["&"] = "\\26";
["'"] = "\\27";
["/"] = "\\2f";
[":"] = "\\3a";
["<"] = "\\3c";
[">"] = "\\3e";
["@"] = "\\40";
["\\"] = "\\5c";
};
local unescapes = {};
for k,v in pairs(escapes) do unescapes[v] = k; end
local function jid_escape(s) return s and (s:gsub(".", escapes)); end
local function jid_unescape(s) return s and (s:gsub("\\%x%x", unescapes)); end
function provider.get_sasl_handler()
local sasl = {};
function sasl:clean_clone() return provider.get_sasl_handler(); end
function sasl:mechanisms() return { PLAIN = true; }; end
function sasl:select(mechanism)
if not self.selected and mechanism == "PLAIN" then
self.selected = mechanism;
return true;
end
end
function sasl:process(message)
if not message then return "failure", "malformed-request"; end
local authorization, authentication, password = message:match("^([^%z]*)%z([^%z]+)%z([^%z]+)");
if not authorization then return "failure", "malformed-request"; end
authentication = saslprep(authentication);
password = saslprep(password);
if (not password) or (password == "") or (not authentication) or (authentication == "") then
return "failure", "malformed-request", "Invalid username or password.";
end
local function test(authentication)
local prepped = nodeprep(authentication);
local normalized = jid_unescape(prepped);
return normalized and provider.test_password(normalized, password) and prepped;
end
local username = test(authentication) or test(jid_escape(authentication));
if username then
self.username = username;
return "success";
end
return "failure", "not-authorized", "Unable to authorize you with the authentication credentials you've sent.";
end
return sasl;
end
module:provides("auth", provider);