Я работаю над переносом старых учетных записей пользователей ALP на новое решение ASP.Net, и я хотел бы, чтобы пользователи могли использовать свои старые пароли.
Однако для того, чтобы это сработало, мне нужно иметь возможность сравнивать старые хэши с недавно вычисленным, основываясь на недавно введенном пароле.
Я искал вокруг и нашел это как реализацию crypt()
вызванного 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); }
И вот моя версия на C # вместе с ожидаемым совпадением.
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; } } } }
Что я делаю не так? Я делаю некоторые большие предположения о работе функций MD5xxxx в версии FreeBSD, но, похоже, это работает.
Это не фактическая версия, используемая PHP? У кого-нибудь есть понимание?
РЕДАКТИРОВАТЬ:
Я загрузил копию исходного кода PHP и обнаружил, что он использует библиотеку glibc. Итак, я загрузил копию исходного кода glibc, нашел функцию __md5_crypt_r, дублировал ее функциональность, и он вернулся с ТОЧНЫМИ же хэшами как версия FreeBSD.
Теперь я очень сильно тупой. Использовал ли PHP 4 другой метод, чем PHP 5? Что происходит?
Хорошо, так вот ответ:
PHP использует glibc- реализацию функции crypt. (прилагается: реализация C #)
Причина, по которой мои старые пароли не соответствуют хешу, заключается в том, что на моем Linux-сервере, на котором находился мой старый сайт (размещенный GoDaddy), был нестандартный алгоритм хэширования. (Возможно, исправить некоторые вещи WEIRD, сделанные в алгоритме.)
Тем не менее, я протестировал следующую реализацию против модульных тестов glibc и против установки Windows на PHP. Оба теста прошли 100%.
РЕДАКТИРОВАТЬ
Вот ссылка: (перешел в Github Gist)
Функция crypt () в PHP использует любой алгоритм хеширования, который базовая операционная система обеспечивает для шифрования данных, – посмотрите его документацию. Итак, первым шагом должно быть выяснить, как данные были зашифрованы (какой алгоритм хеширования использовался). Как только вы это знаете, должно быть тривиально найти тот же алгоритм для C #.
Вы всегда можете использовать system () (или что-то другое, что статическая функция C #) для сценария командной строки php, который выполняет склеп для вас.
Я бы порекомендовал форсировать изменение пароля, хотя после успешного входа в систему. Тогда вы можете иметь флаг, который указывает, изменился ли пользователь. Как только все изменились, вы можете сбросить вызов php.
Просто повторите реализацию php … Убедитесь, что библиотеки crypt php находятся в пути вашей системы …
Возможно, вам понадобится обновить свой метод interop, чтобы убедиться, что ваша строковая маршалинг / кодировка верна … вы можете использовать оригинальный алгоритм хэширования.
[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); ... }
Это не выглядит тривиальным.
UPDATE : Первоначально я писал: « Функция PHP Crypt не похожа на стандартный хэш. Почему бы и нет? Кто знает». Как указано в комментариях, PHP crypt () такой же, как и в BSD для passwd crypt. Я не знаю, является ли это стандартом для дееспособности, но он является стандартом де-факто. Так.
Я придерживаюсь своей позиции, что он не кажется тривиальным.
Вместо того, чтобы переносить код, вы можете рассмотреть возможность сохранения старого PHP и строго использовать его для проверки паролей старых паролей. Когда пользователи меняют свои пароли, используйте новый алгоритм хэширования, что-то более «открытое». Вам нужно будет хранить хэш, а также «аромат хэша» для каждого пользователя.