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?