2009-05-12 2 views
0

Im работает над переносом старых учетных записей ALP на новое решение ASP.Net, и я хотел бы, чтобы пользователи могли использовать свои старые пароли.Проблема портирования функции PHP crypt() на C#

Однако для того, чтобы это сработало, мне нужно иметь возможность сравнивать старые хэши с недавно вычисленным, основываясь на недавно введенном пароле.

Я искал вокруг и нашел this как реализацию 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 = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray(); 

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


    } 
} 

Что я делаю неправильно? Я делаю некоторые большие предположения о работе функций MD5xxxx в версии FreeBSD, но, похоже, это работает.

Это не фактическая версия, используемая PHP? У кого-нибудь есть понимание?

EDIT:

Я скачал копию исходного кода PHP, и обнаружил, что она использует библиотеку GLibC. Итак, я загрузил копию исходного кода glibc, нашел функцию __md5_crypt_r, дублировал ее функциональность, и он вернулся с ТОЧНЫМИ же хэшами как версия FreeBSD.

Теперь я в значительной степени тупик. Использовал ли PHP 4 другой метод, чем PHP 5? Что происходит?

+0

Принимали ли вы, что код C от фактического источника РНР? Вы посмотрели на реализацию crypt()? –

ответ

4

Хорошо, так вот ответ:

PHP использует GLibC реализацию функции крипт. (прилагается: реализация C#)

Причина, по которой мои старые пароли не соответствуют хешу, заключается в том, что на моем ядре Linux, на котором находился мой старый веб-сайт (размещенный GoDaddy), был нестандартный алгоритм хэширования. (Возможно, исправить некоторые вещи WEIRD, сделанные в алгоритме.)

Однако, я протестировал следующую реализацию против модульных тестов glibc и против установки Windows на PHP. Оба теста прошли 100%.

EDIT
Вот ссылка: (переехал в Github Gist)

https://gist.github.com/1092558

+0

"используется для эмуляции символа C * типа", почему бы просто не использовать char * в C#? –

+0

Потому что тогда его нужно будет обернуть в кодовый блок UNSAFE. ОЧЕНЬ нежелательно. –

+0

Пожалуйста, не могли бы вы разместить ссылку на полный код. –

0

Функция crypt() в PHP использует любой алгоритм хеширования, который базовая операционная система обеспечивает для шифрования данных - посмотрите на ее документацию. Итак, первым шагом должно быть выяснить, как данные были зашифрованы (какой алгоритм хеширования использовался). Как только вы это знаете, должно быть тривиально найти тот же алгоритм для C#.

+0

Это на самом деле неверно. Он использует алгоритм DES, MD5 или Blowfish, но сам алгоритм не является прямым хешем. В случае версии MD5 она НЕ использует реализацию операционной системы, а скорее Zend. Если вы прочитаете спецификации, вы увидите, что часть MD5 (тот, который мне нужен) также использует механизм соления, который не совсем ясен. –

+0

Zend-реализация MD5 была представлена ​​в PHP 5.3, пожалуйста, прочитайте руководство. – soulmerge

-1

Это не выглядит тривиальным.

UPDATE: Изначально я писал: «?. Функция PHP Crypt не выглядит как стандартный хэш Почему не кто знает» Как указано в комментариях PHP склепа() такая же, как используется в BSD для passwd crypt. Я не знаю, является ли это стандартом для дееспособности, но он является стандартом де-факто. Так.

Я стою по своему положению, что он не кажется тривиальным.

Вместо того, чтобы портировать код, вы можете рассмотреть возможность сохранения старого PHP и строго использовать его для проверки паролей старых паролей. Когда пользователи меняют свои пароли, используйте новый алгоритм хэширования, что-то более «открытое». Вам нужно будет хранить хэш, а также «аромат хэша» для каждого пользователя.

+0

Почему это было окрашено? Мой - обман, но на практике нет ничего плохого, не изобретая колесо. – jmucchiello

+0

Я не ответил на ваш ответ, но я хотел бы сообщить вам, что он, вероятно, был окрашен, потому что он ни на что не отвечает. В дополнение к тому, что вы не помогаете в фактической задаче переноса функции, ваш пост также предлагает сохранить дополнительную среду (которая уже снята) без реальных средств связи между старыми и новыми приложениями. –

+0

Я пропустил то, что среда PHP была снята. Я работал над вашим четко заявленным требованием, чтобы сравнить старые и новые хеши. Использование существующего PHP будет адресовано этому, поэтому он уверен, что мой ответ является прямым ответом на ваши требования. Я не понимал, что у вас есть жесткое требование для кода PORT. – Cheeso

0

Вы всегда можете вызывать систему() (или любую другую статическую функцию C#) для сценария командной строки php, который выполняет склеп для вас.

Я бы порекомендовал форсировать изменение пароля, хотя после успешного входа в систему. Тогда вы можете иметь флаг, который указывает, изменился ли пользователь. Как только все изменились, вы можете сбросить вызов php.

+0

Мне нравятся эти решения лучше, чем другие хакерские решения, в основном потому, что PHP можно установить как локальный сервер сценариев. Однако в моем случае я не хочу устанавливать PHP в моей производственной среде, чтобы обеспечить тривиальную функцию. (Я собираюсь ответить +1, чтобы вернуть его к нулю.) –

0

Просто повторите реализацию php ... Убедитесь, что библиотеки криптографии 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); 
    ... 
} 
+0

Дело в том, что PHP является языком сценариев, параметры передаются в функции странным образом. Хорошая идея, но я не могу сказать, что я об этом уже не думал. –