%%%------------------------------------------------------------------- %%% File : md5_crypt.erl %%% Author : Sean Hinde %%% Description : %%% %%% Created : 18 Dec 2004 by Sean Hinde %%%------------------------------------------------------------------- %% md5_crypt algorithm adapted from python version : %% http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/325204 %% with reference to original bsd version at: %% http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.5&content-type=text/plain %% @doc This module implements the md5 crypt function used for %% encrypting passwords for various UNIX systems. It is designed to be %% significantly more secure than the old fashioned des based UNIX %% crypt program. -module(md5_crypt). -export([md5_crypt/2, md5_crypt/3, test/0]). %% Itoa64_0 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", %% Itoa64 = list_to_tuple(Itoa64_0), -define(itoa64, {46,47,48,49,50,51,52,53,54,55,56,57,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,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}). %% @doc Returns the encrypted password given the Password as a string %% and a Salt parameter which should be a string of random characters %% of length 8. This will return a string of the form: %%
%% "$1$" ++ Salt ++ "$" ++ Encrypted_password
%% 
%% %% This is the format the password should be stored in the passwd file %% so checking a password is a matter of reading the Salt for that %% user from the passwd file, calculating the md5_crypt of the %% Supplied clear password with the stored salt, and ensuring that the %% output of this routine matches the whole entry in the passwd file. %% %% Equally the process of storing a password is a straightforward %% matter of supplying the password and some random string and storing %% the result of this function against the username %% %% @spec md5_crypt(Password::string(), Salt::string) -> %% Encrypted_passwd::string() md5_crypt(Password, Salt) when is_list(Password), is_list(Salt) -> md5_crypt(Password, Salt, "$1$"). %% @doc As md5_crypt/2 but allows the "1" part of the initial $1$ %% magic string to be replaced with a custom string. e.g. apache uses %% "apr1" which would give a magic string of $apr1$ md5_crypt(Password, Salt, Magic) when is_list(Password), is_list(Salt), is_list(Magic) -> %% The password first, since that is what is most unknown Then our %% magic string Then the raw salt M = erlang:md5_init(), M1 = erlang:md5_update(M, Password ++ Magic ++ Salt), %% Then just as many characters of the MD5(pw,salt,pw) Mixin = erlang:md5(Password ++ Salt ++ Password), % io:format("Mixin = ~p~n",[{Mixin, length(Password)}]), {M3,_} = lists:foldl(fun(_, {M2, I}) -> I1 = I rem 16, <<_:I1/binary, Mix, _/binary>> = Mixin, {erlang:md5_update(M2, [Mix]), I+1} end, {M1, 0}, Password), %% Then something really weird... %% Also really broken, as far as I can tell. -m I = length(Password), P0 = hd(Password), M4 = broken(I, M3, P0), Final = erlang:md5_final(M4), %% io:format("Final = ~p~n",[Final]), Final1 = slowdown(erlang:md5_init(), Password, Final, Salt, 0), Final2 = list_to_tuple(binary_to_list(Final1)), Combs = [{0, 6, 12}, {1, 7, 13}, {2, 8, 14}, {3, 9, 15}, {4, 10, 5}], Rearranged = lists:foldl(fun({A,B,C}, Re) -> V = (element(A+1, Final2) bsl 16) bor (element(B+1, Final2) bsl 8) bor element(C+1, Final2), Re ++ to_64(V, 4) end, [], Combs), V = element(11+1, Final2), Rearranged2 = Rearranged ++ to_64(V, 2), Magic ++ Salt ++ "$" ++ Rearranged2. broken(0, M, P) -> M; broken(I, M, P) -> if (I band 1) /=0 -> broken(I bsr 1, erlang:md5_update(M, <<0>>), P); true -> broken(I bsr 1, erlang:md5_update(M, <

>), P) end. slowdown(M, Pass, Final, Salt, 1000) -> Final; slowdown(M, Pass, Final, Salt, I) -> M1 = if (I band 1) == 1 -> erlang:md5_update(M, Pass); true -> erlang:md5_update(M, Final) end, M2 = if (I rem 3) /= 0 -> erlang:md5_update(M1, Salt); true -> M1 end, M3 = if (I rem 7) /= 0 -> erlang:md5_update(M2, Pass); true -> M2 end, M4 = if (I band 1) == 1 -> erlang:md5_update(M3, Final); true -> erlang:md5_update(M3, Pass) end, Final1 = erlang:md5_final(M4), slowdown(erlang:md5_init(), Pass, Final1, Salt, I+1). to_64(V, 0) -> []; to_64(V, N) -> [element((V band 16#3f) + 1, ?itoa64)|to_64(V bsr 6, N-1)]. %% @doc Perform a set of standard compliance tests for the %% implementation. Not useful at runtime. test() -> Tests = [{" ", "$1$yiiZbNIH$YiCsHZjcTkYd31wkgW8JF."}, {"pass", "$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90"}, {"____fifteen____", "$1$s9lUWACI$Kk1jtIVVdmT01p0z3b/hw1"}, {"____sixteen_____", "$1$dL3xbVZI$kkgqhCanLdxODGq14g/tW1"}, {"____seventeen____", "$1$NaH5na7J$j7y8Iss0hcRbu3kzoJs5V."}, {"__________thirty-three___________", "$1$HO7Q6vzJ$yGwp2wbL5D7eOVzOmxpsy."}, {"apache", "$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1"}], lists:foreach(fun({Clear, Hash}) -> case test(Clear, Hash) of true -> io:format("~s: pass~n", [Clear]); false -> io:format("~s: FAIL~n", [Clear]) end end, Tests). test(Clear, Hash) -> [M,S,_] = string:tokens(Hash, "$"), H = md5_crypt(Clear,S,[$$|M] ++ "$"), H == Hash.