views:

711

answers:

5

Hi fellas,

Im working on porting some old ALP user accounts to a new ASP.Net solution, and I would like for the users to be able to use their old passwords.

However, in order for that to work, I need to be able to compare the old hashes to a newly calculated one, based on a newly typed password.

I searched around, and found this as the implementation of crypt() called by PHP:

char *
crypt_md5(const char *pw, const char *salt)
{
    MD5_CTX ctx,ctx1;
    unsigned long l;
    int sl, pl;
    u_int i;
    u_char final[MD5_SIZE];
    static const char *sp, *ep;
    static char passwd[120], *p;
    static const char *magic = "$1$";

    /* Refine the Salt first */
    sp = salt;

    /* If it starts with the magic string, then skip that */
    if(!strncmp(sp, magic, strlen(magic)))
     sp += strlen(magic);

    /* It stops at the first '$', max 8 chars */
    for(ep = sp; *ep && *ep != '$' && ep < (sp + 8); ep++)
     continue;

    /* get the length of the true salt */
    sl = ep - sp;

    MD5Init(&ctx);

    /* The password first, since that is what is most unknown */
    MD5Update(&ctx, (const u_char *)pw, strlen(pw));

    /* Then our magic string */
    MD5Update(&ctx, (const u_char *)magic, strlen(magic));

    /* Then the raw salt */
    MD5Update(&ctx, (const u_char *)sp, (u_int)sl);

    /* Then just as many characters of the MD5(pw,salt,pw) */
    MD5Init(&ctx1);
    MD5Update(&ctx1, (const u_char *)pw, strlen(pw));
    MD5Update(&ctx1, (const u_char *)sp, (u_int)sl);
    MD5Update(&ctx1, (const u_char *)pw, strlen(pw));
    MD5Final(final, &ctx1);
    for(pl = (int)strlen(pw); pl > 0; pl -= MD5_SIZE)
     MD5Update(&ctx, (const u_char *)final,
         (u_int)(pl > MD5_SIZE ? MD5_SIZE : pl));

    /* Don't leave anything around in vm they could use. */
    memset(final, 0, sizeof(final));

    /* Then something really weird... */
    for (i = strlen(pw); i; i >>= 1)
     if(i & 1)
         MD5Update(&ctx, (const u_char *)final, 1);
     else
         MD5Update(&ctx, (const u_char *)pw, 1);

    /* Now make the output string */
    strcpy(passwd, magic);
    strncat(passwd, sp, (u_int)sl);
    strcat(passwd, "$");

    MD5Final(final, &ctx);

    /*
     * and now, just to make sure things don't run too fast
     * On a 60 Mhz Pentium this takes 34 msec, so you would
     * need 30 seconds to build a 1000 entry dictionary...
     */
    for(i = 0; i < 1000; i++) {
     MD5Init(&ctx1);
     if(i & 1)
      MD5Update(&ctx1, (const u_char *)pw, strlen(pw));
     else
      MD5Update(&ctx1, (const u_char *)final, MD5_SIZE);

     if(i % 3)
      MD5Update(&ctx1, (const u_char *)sp, (u_int)sl);

     if(i % 7)
      MD5Update(&ctx1, (const u_char *)pw, strlen(pw));

     if(i & 1)
      MD5Update(&ctx1, (const u_char *)final, MD5_SIZE);
     else
      MD5Update(&ctx1, (const u_char *)pw, strlen(pw));
     MD5Final(final, &ctx1);
    }

    p = passwd + strlen(passwd);

    l = (final[ 0]<<16) | (final[ 6]<<8) | final[12];
    _crypt_to64(p, l, 4); p += 4;
    l = (final[ 1]<<16) | (final[ 7]<<8) | final[13];
    _crypt_to64(p, l, 4); p += 4;
    l = (final[ 2]<<16) | (final[ 8]<<8) | final[14];
    _crypt_to64(p, l, 4); p += 4;
    l = (final[ 3]<<16) | (final[ 9]<<8) | final[15];
    _crypt_to64(p, l, 4); p += 4;
    l = (final[ 4]<<16) | (final[10]<<8) | final[ 5];
    _crypt_to64(p, l, 4); p += 4;
    l = final[11];
    _crypt_to64(p, l, 2); p += 2;
    *p = '\0';

    /* Don't leave anything around in vm they could use. */
    memset(final, 0, sizeof(final));

    return (passwd);
}

And, here is my version in C#, along with an expected match.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Security.Cryptography;
using System.IO;
using System.Management;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] salt = Encoding.ASCII.GetBytes("$1$ls3xPLpO$Wu/FQ.PtP2XBCqrM.w847/");
            Console.WriteLine("Hash:  " + Encoding.ASCII.GetString(salt));

            byte[] passkey = Encoding.ASCII.GetBytes("suckit");

            byte[] newhash = md5_crypt(passkey, salt);
            Console.WriteLine("Hash2: " + Encoding.ASCII.GetString(newhash));

            byte[] newhash2 = md5_crypt(passkey, newhash);
            Console.WriteLine("Hash3: " + Encoding.ASCII.GetString(newhash2));


            Console.ReadKey(true);
        }

        public static byte[] md5_crypt(byte[] pw, byte[] salt)
        {
            MemoryStream ctx, ctx1;
            ulong l;
            int sl, pl;
            int i;
            byte[] final;
            int sp, ep; //** changed pointers to array indices
            MemoryStream passwd = new MemoryStream();
            byte[] magic = Encoding.ASCII.GetBytes("$1$");

            // Refine the salt first
            sp = 0;  //** Changed to an array index, rather than a pointer.

            // If it starts with the magic string, then skip that
            if (salt[0] == magic[0] &&
                salt[1] == magic[1] &&
                salt[2] == magic[2])
            {
                sp += magic.Length;
            }

            // It stops at the first '$', max 8 chars
            for (ep = sp;
                (ep + sp < salt.Length) &&  //** Converted to array indices, and rather than check for null termination, check for the end of the array.
                salt[ep] != (byte)'$' &&
                ep < (sp + 8);
                ep++)
                continue;

            // Get the length of the true salt
            sl = ep - sp;

            ctx = MD5Init();

            // The password first, since that is what is most unknown
            MD5Update(ctx, pw, pw.Length);

            // Then our magic string
            MD5Update(ctx, magic, magic.Length);

            // Then the raw salt
            MD5Update(ctx, salt, sp, sl);

            // Then just as many characters of the MD5(pw,salt,pw)
            ctx1 = MD5Init();
            MD5Update(ctx1, pw, pw.Length);
            MD5Update(ctx1, salt, sp, sl);
            MD5Update(ctx1, pw, pw.Length);
            final = MD5Final(ctx1);
            for(pl = pw.Length; pl > 0; pl -= final.Length)
                MD5Update(ctx, final, 
                    (pl > final.Length ? final.Length : pl));

            // Don't leave anything around in vm they could use.
            for (i = 0; i < final.Length; i++) final[i] = 0;

            // Then something really weird...
            for (i = pw.Length; i != 0; i >>= 1)
             if((i & 1) != 0)
                 MD5Update(ctx, final, 1);
             else
                 MD5Update(ctx, pw, 1);


            // Now make the output string
            passwd.Write(magic, 0, magic.Length);
            passwd.Write(salt, sp, sl);
            passwd.WriteByte((byte)'$');

            final = MD5Final(ctx);

            // and now, just to make sure things don't run too fast
            // On a 60 Mhz Pentium this takes 34 msec, so you would
            // need 30 seconds to build a 1000 entry dictionary...
            for(i = 0; i < 1000; i++)
            {
             ctx1 = MD5Init();
             if((i & 1) != 0)
              MD5Update(ctx1, pw, pw.Length);
             else
              MD5Update(ctx1, final, final.Length);

             if((i % 3) != 0)
              MD5Update(ctx1, salt, sp, sl);

             if((i % 7) != 0)
              MD5Update(ctx1, pw, pw.Length);

             if((i & 1) != 0)
              MD5Update(ctx1, final, final.Length);
             else
              MD5Update(ctx1, pw, pw.Length);

                final = MD5Final(ctx1);
            }

            //** Section changed to use a memory stream, rather than a byte array.
            l = (((ulong)final[0]) << 16) | (((ulong)final[6]) << 8) | ((ulong)final[12]);
            _crypt_to64(passwd, l, 4);
            l = (((ulong)final[1]) << 16) | (((ulong)final[7]) << 8) | ((ulong)final[13]);
            _crypt_to64(passwd, l, 4);
            l = (((ulong)final[2]) << 16) | (((ulong)final[8]) << 8) | ((ulong)final[14]);
            _crypt_to64(passwd, l, 4);
            l = (((ulong)final[3]) << 16) | (((ulong)final[9]) << 8) | ((ulong)final[15]);
            _crypt_to64(passwd, l, 4);
            l = (((ulong)final[4]) << 16) | (((ulong)final[10]) << 8) | ((ulong)final[5]);
            _crypt_to64(passwd, l, 4);
            l = final[11];
            _crypt_to64(passwd, l, 2);

            byte[] buffer = new byte[passwd.Length];
            passwd.Seek(0, SeekOrigin.Begin);
            passwd.Read(buffer, 0, buffer.Length);
            return buffer;
        }

        public static MemoryStream MD5Init()
        {
            return new MemoryStream();
        }

        public static void MD5Update(MemoryStream context, byte[] source, int length)
        {
            context.Write(source, 0, length);
        }

        public static void MD5Update(MemoryStream context, byte[] source, int offset, int length)
        {
            context.Write(source, offset, length);
        }

        public static byte[] MD5Final(MemoryStream context)
        {
            long location = context.Position;
            byte[] buffer = new byte[context.Length];
            context.Seek(0, SeekOrigin.Begin);
            context.Read(buffer, 0, (int)context.Length);
            context.Seek(location, SeekOrigin.Begin);
            return MD5.Create().ComputeHash(buffer);
        }

        // Changed to use a memory stream rather than a character array.
        public static void _crypt_to64(MemoryStream s, ulong v, int n)
        {
            char[] _crypt_a64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray(); 

            while (--n >= 0)
            {
             s.WriteByte((byte)_crypt_a64[v & 0x3f]);
             v >>= 6;
            }
        }


    }
}

What Am I doing wrong? I am making some big assumptions about the workings of the MD5xxxx functions in the FreeBSD version, but it seems to work.

Is this not the actual version used by PHP? Does anyone have any insight?

EDIT:

I downloaded a copy of PHP's source code, and found that it uses the glibc library. So, I downloaded a copy of glibc's source code, found the __md5_crypt_r function, duplicated its functionality, ant it came back with the EXACT same hashes as the FreeBSD version.

Now, I am pretty much stumped. Did PHP 4 use a different method than PHP 5? What is going on?

A: 

The crypt() function in PHP uses whatever hash algorithm the underlying operating system provides for encrypting the data - have a look at its documentation. So the first step should be to find out, how the data was encrypted (what hashing algorithm was used). Once you know that, it should be trivial to find the same algorithm for C#.

soulmerge
That is actually not correct. It does use a DES, MD5 or Blowfish algorithm, but the actual algorithm itself is not a straight forward hash. In the case of the MD5 version it DOES NOT use the operating system's implementation, but rather Zend's own. If you read the specs, you would see that the MD5 portion (the one I need) also uses a salting mechanism that is not entirely clear.
John Gietzen
The Zend-implementation of MD5 was introduced in PHP 5.3, please read the manual.
soulmerge
A: 

It does not look trivial.

UPDATE: Originally I wrote: "The PHP Crypt function does not look like a standard hash. Why not? Who knows." As pointed out in the comments, the PHP crypt() is the same as used in BSD for passwd crypt. I don't know if that is a dejure standard, but it is defacto standard. So.

I stand by my position that it does not appear to be trivial.

Rather than porting the code, you might consider keeping the old PHP running, and use it strictly for password validation of old passwords. As users change their passwords, use a new hashing algorithm, something a little more "open". You would have to store the hash, as well as the "flavor of hash" for each user.

Cheeso
Why was this dinged? Mine is a dupe but there's nothing wrong in practice with not reinventing the wheel.
jmucchiello
I did not ding your answer, but I would like to let you know that it was probably dinged because it does not answer the question in the least. In addition to not being helpful to the actual task of porting the function, your post also suggests keeping an additional environment (which has already been taken down) running with no real means of communication between the old and new applications.
John Gietzen
I missed the point that the PHP environment was taken down. I was working off your clearly stated requirement that you need to compare the old and new hashes. Using the existing PHP would address that, so it sure feels like my response is a direct answer to your requirement. I did not understand that you had a hard requirement to PORT code.
Cheeso
I'm not the one who dinged it, but I'm going to note that "The PHP Crypt function does not look like a standard hash" is wrong, as this is the code posted *is* the standard way to do salted md5 crypt in the BSDs and Linux, as used by the passwd command.
R. Bemrose
Well then I deserved to be dinged!
Cheeso
You are right that it wasn't a trivial implementation.However, it should have been (in my opinion).They do all sorts of crazy hashing and churning that is entirely unnecessary when just adding a pad and a salt would provide the security desired.
John Gietzen
That's what I thought !
Cheeso
A: 

You can always system() (or whatever the C# static function is called) out to a php command-line script that does the crypt for you.

I would recommend forcing a password change though after successful login. Then you can have a flag that indicates if the user has changed. Once everyone has changed you can dump the php call.

jmucchiello
I like this solutions better than the other hackish solutions, mostly because PHP can be installed as a local scripting host. However, in my case I do not want to install PHP on my production environment just to provide a trivial function. (I'm going to +1 your answer, to bring it back to zero.)
John Gietzen
A: 

Just reuse the php implementation... Make sure php's crypt libraries are in your system environment path...

You may need to update your interop method to make sure your string marshaling/charset is correct... you can then use the original hashing algorithm.

[DllImport("crypt.dll", CharSet=CharSet.ASCII)]
private static extern string crypt(string password, string salt);

public bool ValidLogin(string username, string password)
{
    string hash = crypt(password, null);
    ...
}
Tracker1
The thing is, since PHP is a scripting language, the parameters are passed to the functions in strange ways.Nice idea, but I can't say I didn't already think of that.
John Gietzen
A: 

Alright, so here is the answer:

PHP uses the glibc implementation of the crypt function. (attached: C# implementation)

The reason my old passwords are not matching the hash is because the Linux box my old website (hosted by GoDaddy) sat on had a non-standard hashing algorithm. (Possibly to fix some of the WEIRD stuff done in the algorithm.)

However, I have tested the following implementation against glibc's unit tests and against a windows install of PHP. Both tests were passed 100%.

EDIT
Here is the link:

http://files.lanlordz.net/Crew/otac0n/UnixCrypt/

John Gietzen
"used to emulate C's char* type", why not simply use char* in C# ?
VirtualBlackFox
Because then it would need to be wrapped in an UNSAFE code block. VERY undesirable here.
John Gietzen
Please could you post a link to the complete code.
Al
@Al, The link is finally posted. Sorry it took so long.
John Gietzen