2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
9 local datamanager
= require
"util.datamanager";
10 local storagemanager
= require
"core.storagemanager";
11 local log = require
"util.logger".init("auth_internal_yubikey");
14 local ipairs
= ipairs
;
15 local hashes
= require
"util.hashes";
16 local jid
= require
"util.jid";
17 local jid_bare
= require
"util.jid".bare
;
18 local config
= require
"core.configmanager";
19 local usermanager
= require
"core.usermanager";
20 local new_sasl
= require
"util.sasl".new
;
23 local prosody
= _G
.prosody
;
25 local yubikey
= require
"yubikey".new_authenticator({
26 prefix_length
= module
:get_option_number("yubikey_prefix_length", 0);
27 check_credentials
= function (ret
, state
, data
)
28 local account
= data
.account
;
29 local yubikey_hash
= hashes
.sha1(ret
.public_id
..ret
.private_id
..(ret
.password
or ""), true);
30 if yubikey_hash
== account
.yubikey_hash
then
33 return false, "invalid-otp";
35 store_device_info
= function (state
, data
)
36 local new_account
= {};
37 for k
, v
in pairs(data
.account
) do
40 new_account
.yubikey_state
= state
;
41 datamanager
.store(data
.username
, data
.host
, "accounts", new_account
);
45 local global_yubikey_key
= module
:get_option_string("yubikey_key");
47 local host
= module
.host
;
49 log("debug", "initializing default authentication provider for host '%s'", host
);
51 function provider
.test_password(username
, password
)
52 log("debug", "test password '%s' for user %s at host %s", password
, username
, module
.host
);
54 local account_info
= datamanager
.load(username
, host
, "accounts") or {};
55 local yubikey_key
= account_info
.yubikey_key
or global_yubikey_key
;
56 if account_info
.yubikey_key
then
57 log("debug", "Authenticating Yubikey OTP for %s", username
);
58 local authed
, err
= yubikey
:authenticate(password
, account_info
.yubikey_key
, account_info
.yubikey_state
or {}, { account
= account_info
, username
= username
, host
= host
});
60 log("debug", "Failed to authenticate %s via OTP: %s", username
, err
);
64 elseif account_info
.password
and password
== account_info
.password
then
65 -- No yubikey configured for this user, treat as normal password
66 log("debug", "No yubikey configured for %s, successful login using password auth", username
);
69 return nil, "Auth failed. Invalid username or password.";
73 function provider
.get_password(username
)
74 log("debug", "get_password for username '%s' at host '%s'", username
, module
.host
);
75 return (datamanager
.load(username
, host
, "accounts") or {}).password
;
78 function provider
.set_password(username
, password
)
79 local account
= datamanager
.load(username
, host
, "accounts");
81 account
.password
= password
;
82 return datamanager
.store(username
, host
, "accounts", account
);
84 return nil, "Account not available.";
87 function provider
.user_exists(username
)
88 local account
= datamanager
.load(username
, host
, "accounts");
90 log("debug", "account not found for username '%s' at host '%s'", username
, module
.host
);
91 return nil, "Auth failed. Invalid username";
96 function provider
.create_user(username
, password
)
97 return datamanager
.store(username
, host
, "accounts", {password
= password
});
100 function provider
.delete_user(username
)
101 return datamanager
.store(username
, host
, "accounts", nil);
104 function provider
.get_sasl_handler()
105 local realm
= module
:get_option("sasl_realm") or module
.host
;
106 local getpass_authentication_profile
= {
107 plain_test
= function(sasl
, username
, password
, realm
)
108 return usermanager
.test_password(username
, realm
, password
), true;
111 return new_sasl(realm
, getpass_authentication_profile
);
114 module
:provides("auth", provider
);
116 function module
.command(arg
)
117 local command
= arg
[1];
118 table.remove(arg
, 1);
119 if command
== "associate" then
120 local user_jid
= arg
[1];
121 if not user_jid
or user_jid
== "help" then
122 prosodyctl
.show_usage([[mod_auth_internal_yubikey associate JID]], [[Set the Yubikey details for a user]]);
126 local username
, host
= jid
.prepped_split(user_jid
);
127 if not username
or not host
then
128 print("Invalid JID: "..user_jid
);
132 local password
, public_id
, private_id
, key
;
135 local k
, v
= arg
[i
]:match("^%-%-(%w+)=(.*)$");
137 k
, v
= arg
[i
]:match("^%-(%w)(.*)$");
139 if k
== "password" then
141 elseif k
== "fixed" then
143 elseif k
== "uid" then
145 elseif k
== "key" or k
== "a" then
151 print(":: Password ::");
152 print("This is an optional password that should be always");
153 print("entered during login *before* the yubikey password.");
154 print("If the yubikey is lost/stolen, unless the attacker");
155 print("knows this prefix, they cannot access the account.");
157 password
= prosodyctl
.read_password();
164 if not public_id
then
165 print(":: Public Yubikey ID ::");
166 print("This is a fixed string of characters between 0 and 16");
167 print("bytes long that the Yubikey prefixes to every token.");
168 print("The ID should be entered in modhex encoding, meaning ");
169 print("a string up to 32 characters. This *must* match");
170 print("exactly the fixed string programmed into the yubikey.");
172 io
.write("Enter fixed id (modhex): ");
174 public_id
= io
.read("*l");
175 if #public_id
> 32 then
176 print("The fixed id must be 32 characters or less. Please try again.");
177 elseif public_id
:match("[^cbdefghijklnrtuv]") then
178 print("The fixed id contains invalid characters. It must be entered in modhex encoding. Please try again.");
185 if not private_id
then
186 print(":: Private Yubikey ID ::");
187 print("This is a fixed secret UID programmed into the yubikey");
188 print("during configuration. It must be entered in hex (not modhex)");
189 print("encoding. It is always 6 bytes long, which is 12 characters");
190 print("in hex encoding.");
193 io
.write("Enter private UID (hex): ");
194 private_id
= io
.read("*l");
195 if #private_id
~= 12 then
196 print("The id length must be 12 characters in hex encoding. Please try again.");
197 elseif private_id
:match("%X") then
198 print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again.");
206 print(":: AES Encryption Key ::");
207 print("This is the secret key that the Yubikey uses to encrypt the");
208 print("generated tokens. It is 32 characters in hex encoding.");
211 io
.write("Enter AES key (hex): ");
214 print("The key length must be 32 characters in hex encoding. Please try again.");
215 elseif key
:match("%X") then
216 print("The key contains invalid characters - it must be in hex encoding (not modhex). Please try again.");
223 local hash
= hashes
.sha1(public_id
..private_id
..password
, true);
228 storagemanager
.initialize_host(host
);
229 local ok
, err
= datamanager
.store(username
, host
, "accounts", account
);
231 print("Error saving configuration:");