util.signal: Update version in header, and author's email
[prosody.git] / util / sasl / scram.lua
index a347e2f3b98a3969e115a9e3d22764260668d62e..4f80052908cd7148b8d123edfaa5e61f36d02a48 100644 (file)
@@ -19,8 +19,13 @@ local xor = require "bit".bxor
 local hmac_sha1 = require "util.hmac".sha1;
 local sha1 = require "util.hashes".sha1;
 local generate_uuid = require "util.uuid".generate;
+local saslprep = require "util.encodings".stringprep.saslprep;
+local log = require "util.logger".init("sasl");
+local t_concat = table.concat;
+local char = string.char;
+local byte = string.byte;
 
-module "plain"
+module "scram"
 
 --=========================
 --SASL SCRAM-SHA-1 according to draft-ietf-sasl-scram-10
@@ -34,17 +39,19 @@ local function bp( b )
        return result
 end
 
+local xor_map = {0;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;1;0;3;2;5;4;7;6;9;8;11;10;13;12;15;14;2;3;0;1;6;7;4;5;10;11;8;9;14;15;12;13;3;2;1;0;7;6;5;4;11;10;9;8;15;14;13;12;4;5;6;7;0;1;2;3;12;13;14;15;8;9;10;11;5;4;7;6;1;0;3;2;13;12;15;14;9;8;11;10;6;7;4;5;2;3;0;1;14;15;12;13;10;11;8;9;7;6;5;4;3;2;1;0;15;14;13;12;11;10;9;8;8;9;10;11;12;13;14;15;0;1;2;3;4;5;6;7;9;8;11;10;13;12;15;14;1;0;3;2;5;4;7;6;10;11;8;9;14;15;12;13;2;3;0;1;6;7;4;5;11;10;9;8;15;14;13;12;3;2;1;0;7;6;5;4;12;13;14;15;8;9;10;11;4;5;6;7;0;1;2;3;13;12;15;14;9;8;11;10;5;4;7;6;1;0;3;2;14;15;12;13;10;11;8;9;6;7;4;5;2;3;0;1;15;14;13;12;11;10;9;8;7;6;5;4;3;2;1;0;};
+
+local result = {};
 local function binaryXOR( a, b )
-       if a:len() > b:len() then
-               b = string.rep("\0", a:len() - b:len())..b
-       elseif string.len(a) < string.len(b) then
-               a = string.rep("\0", b:len() - a:len())..a
-       end
-       local result = ""
-       for i=1, a:len() do
-               result = result..string.char(xor(a:byte(i), b:byte(i)))
+       for i=1, #a do
+               local x, y = byte(a, i), byte(b, i);
+               local lowx, lowy = x % 16, y % 16;
+               local hix, hiy = (x - lowx) / 16, (y - lowy) / 16;
+               local lowr, hir = xor_map[lowx * 16 + lowy + 1], xor_map[hix * 16 + hiy + 1];
+               local r = hir * 16 + lowr;
+               result[i] = char(r)
        end
-       return result
+       return t_concat(result);
 end
 
 -- hash algorithm independent Hi(PBKDF2) implementation
@@ -52,7 +59,7 @@ local function Hi(hmac, str, salt, i)
        local Ust = hmac(str, salt.."\0\0\0\1");
        local res = Ust;        
        for n=1,i-1 do
-               Und = hmac(str, Ust)
+               local Und = hmac(str, Ust)
                res = binaryXOR(res, Und)
                Ust = Und
        end
@@ -68,8 +75,11 @@ local function validate_username(username)
        end
        
        -- replace =2D with , and =3D with =
+       username = username:gsub("=2D", ",");
+       username = username:gsub("=3D", "=");
        
        -- apply SASLprep
+       username = saslprep(username);
        return username;
 end
 
@@ -83,10 +93,16 @@ local function scram_sha_1(self, message)
                self.state["name"] = client_first_message:match("n=(.+),r=")
                self.state["clientnonce"] = client_first_message:match("r=([^,]+)")
                
-               self.state.name = validate_username(self.state.name);
                if not self.state.name or not self.state.clientnonce then
                        return "failure", "malformed-request";
                end
+               
+               self.state.name = validate_username(self.state.name);
+               if not self.state.name then
+                       log("debug", "Username violates either SASLprep or contains forbidden character sequences.")
+                       return "failure", "malformed-request", "Invalid username.";
+               end
+               
                self.state["servernonce"] = generate_uuid();
                self.state["salt"] = generate_uuid();
                
@@ -102,14 +118,19 @@ local function scram_sha_1(self, message)
                self.state["nonce"] = client_final_message:match("r=(.+),p=");
                self.state["channelbinding"] = client_final_message:match("c=(.+),r=");
                if not self.state.proof or not self.state.nonce or not self.state.channelbinding then
-                       return "failure", "malformed-request";
+                       return "failure", "malformed-request", "Missing an attribute(p, r or c) in SASL message.";
                end
                
-               local password;
+               local password, state;
                if self.profile.plain then
                        password, state = self.profile.plain(self.state.name, self.realm)
                        if state == nil then return "failure", "not-authorized"
                        elseif state == false then return "failure", "account-disabled" end
+                       password = saslprep(password);
+                       if not password then
+                               log("debug", "Password violates SASLprep.");
+                               return "failure", "not-authorized", "Invalid password."
+                       end
                end
                
                local SaltedPassword = Hi(hmac_sha1, password, self.state.salt, default_i)