2009-05-12 13 views
0

Ich arbeite an der Portierung einige alte ALP Benutzerkonten zu einer neuen ASP.Net-Lösung, und ich möchte für die Benutzer in der Lage sein, ihre alten Passwörter verwenden.Problem Portierung PHP crypt() -Funktion zu C#

Damit dies jedoch funktioniert, muss ich in der Lage sein, die alten Hashes mit einem neu berechneten zu vergleichen, basierend auf einem neu eingegebenen Passwort.

Ich suchte herum, und fand this als die Umsetzung von crypt() von PHP genannt:

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); 
} 

Und hier ist meine Version in C#, zusammen mit einem erwarteten Spiel.

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; 
      } 
     } 


    } 
} 

Was mache ich falsch? Ich mache einige große Annahmen über die Funktionsweise der MD5xxxx-Funktionen in der FreeBSD-Version, aber es scheint zu funktionieren.

Ist dies nicht die tatsächliche Version von PHP? Hat jemand einen Einblick?

EDIT:

heruntergeladen ich eine Kopie des Quellcodes von PHP, und stellte fest, dass es die glibc-Bibliothek verwendet. Also habe ich eine Kopie des Quellcodes von glibc heruntergeladen, die __md5_crypt_r-Funktion gefunden, ihre Funktionalität dupliziert und sie kam mit den EXACT-Hashwerten der FreeBSD-Version zurück.

Jetzt bin ich ziemlich ratlos. Hatte PHP 4 eine andere Methode als PHP 5? Was ist los?

+0

Haben Sie nehmen, dass C-Code aus PHP eigentliche Quelle? Hast du die Implementierung von crypt() dort angeschaut? –

Antwort

4

Okay, so ist hier die Antwort:

PHP verwendet die glibc Umsetzung der Krypta Funktion. (im Anhang: C# -Implementierung)

Der Grund, warum meine alten Passwörter nicht mit dem Hash übereinstimmen, liegt daran, dass die alte Linux-Box (auf der GoDaddy gehostet wurde) einen nicht standardisierten Hashalgorithmus hatte. (Möglicherweise einige der WEIRD Sachen in den Algorithmus getan zu beheben.)

Allerdings habe ich die folgende Implementierung gegen Glibc Unit Tests und gegen eine Windows-Installation von PHP getestet. Beide Tests wurden zu 100% bestanden.

EDIT
Hier ist der Link: (verschoben auf einen Github Gist)

https://gist.github.com/1092558

+0

"verwendet, um C char * Typ zu emulieren, warum nicht einfach char * in C#? –

+0

Denn dann müsste es in einen UNSAFE-Codeblock verpackt werden. Sehr unerwünscht hier. –

+0

Bitte können Sie einen Link zum vollständigen Code veröffentlichen. –

0

Die Funktion crypt() in PHP verwendet einen Hash-Algorithmus, den das zugrunde liegende Betriebssystem zum Verschlüsseln der Daten bietet - siehe Dokumentation. Der erste Schritt sollte sein, herauszufinden, wie die Daten verschlüsselt wurden (welcher Hash-Algorithmus wurde verwendet). Sobald Sie das wissen, sollte es trivial sein, den gleichen Algorithmus für C# zu finden.

+0

Das ist eigentlich nicht korrekt. Es verwendet einen DES-, MD5- oder Blowfish-Algorithmus, aber der eigentliche Algorithmus selbst ist kein direkter Hash. Bei der MD5-Version wird NICHT die Implementierung des Betriebssystems, sondern Zend verwendet. Wenn Sie die Spezifikationen lesen, würden Sie sehen, dass der MD5-Teil (den ich brauche) auch einen Salting-Mechanismus verwendet, der nicht ganz klar ist. –

+0

Die Zend-Implementierung von MD5 wurde in PHP 5.3 eingeführt, bitte lesen Sie das Handbuch. – soulmerge

-1

Es sieht nicht trivial aus.

UPDATE: Ursprünglich schrieb ich: „?. Die Funktion PHP Crypt sieht nicht wie ein Standard-Hash Warum nicht Wer weiß“ Wie in den Kommentaren darauf hingewiesen, ist der PHP-crypt() die gleiche wie in BSD für Passwd Crypt verwendet. Ich weiß nicht, ob das ein Dejure-Standard ist, aber es ist Defacto-Standard. Damit.

Ich stehe zu meiner Position, dass es nicht trivial zu sein scheint.

Anstatt den Code zu portieren, sollten Sie in Erwägung ziehen, die alte PHP-Version weiterhin zu verwenden und sie ausschließlich für die Kennwortüberprüfung alter Kennwörter zu verwenden. Wenn Benutzer ihre Passwörter ändern, verwenden Sie einen neuen Hashalgorithmus, etwas "offener". Sie müssten den Hash sowie den "Geschmack von Hash" für jeden Benutzer speichern.

+0

Warum war das so? Meine ist ein Betrogener, aber in der Praxis ist nichts falsch daran, das Rad nicht neu zu erfinden. – jmucchiello

+0

Ich habe Ihre Antwort nicht geäußert, aber ich möchte Sie wissen lassen, dass es wahrscheinlich sehr beleidigt war, weil es die Frage nicht im Geringsten beantwortet. Zusätzlich zu der Tatsache, dass sie für die eigentliche Aufgabe der Portierung der Funktion nicht hilfreich ist, schlägt Ihr Beitrag auch vor, eine zusätzliche Umgebung (die bereits abgebaut wurde) ohne echte Kommunikationsmittel zwischen der alten und der neuen Anwendung laufen zu lassen. –

+0

Ich habe den Punkt übersehen, dass die PHP-Umgebung abgebaut wurde. Ich arbeitete an Ihrer klar formulierten Anforderung, dass Sie die alten und neuen Hashes vergleichen müssen. Mit dem vorhandenen PHP würde das angesprochen werden, also fühlt es sich so an, als wäre meine Antwort eine direkte Antwort auf Ihre Anforderung. Ich habe nicht verstanden, dass du eine harte Anforderung an den PORT-Code hast. – Cheeso

0

Sie können immer system() (oder was auch immer die statische C# -Funktion genannt wird) zu einem PHP-Befehlszeilenskript, das die Krypta für Sie ausführt.

Ich würde empfehlen, eine Passwortänderung nach erfolgreicher Anmeldung erzwingen. Dann können Sie ein Flag haben, das anzeigt, ob der Benutzer sich geändert hat. Sobald sich alle geändert haben, können Sie den php-Anruf dumpen.

+0

Ich mag diese Lösungen besser als die anderen Hackish-Lösungen, vor allem weil PHP als lokaler Scripting-Host installiert werden kann. In meinem Fall möchte ich jedoch PHP nicht in meiner Produktionsumgebung installieren, nur um eine triviale Funktion bereitzustellen. (Ich werde Ihre Antwort +1 geben, um es auf Null zurück zu bringen.) –

0

Verwenden Sie einfach die PHP-Implementierung ... Stellen Sie sicher, dass die crypt-Bibliotheken von php in Ihrem Systemumgebungspfad enthalten sind ...

Möglicherweise müssen Sie Ihre Interop-Methode aktualisieren, um sicherzustellen, dass Ihr String Marshalling/Charset korrekt ist ... Sie können dann den ursprünglichen Hashing-Algorithmus verwenden.

[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

Die Sache ist, da PHP eine Skriptsprache ist, werden die Parameter auf seltsame Weise an die Funktionen übergeben. Schöne Idee, aber ich kann nicht sagen, dass ich nicht schon darüber nachgedacht habe. –