Raketenwilli: PHP-Funktion password_hash für Python

(Vorab: Die Sache drängelt nicht…)

Ist zufällig mal jemand über eine Funktion, Klasse oder Libary gestolpert, mit der in Python Passwort-Hashes für PHP (und visa versa) erzeugt und verifiziert werden können?

Die hashlib und passlib hab ich gefunden, das Zeug erzeugt aber Passwörter, die mit $2b$ beginnen - die mag password_verify() von PHP nicht.

Hier mein Versuch aus dem tmp-Ordner (unten, class php_password), der aber auch nicht klug macht...

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import hashlib
import base64
import random
import re

import passlib
import bcrypt


class password:

	def hash( password, hashmethod='sha256', rounds=6, saltLen=22 ):
		
		minRounds     = 5000
		minSaltLen    = 16
		unsafeMethods ={ 'md5', 'sha1', 'shake_128' }
		
		if not hashmethod in hashlib.algorithms_guaranteed:
			print ( "Fatal: Methode (2. Argument) ist nicht in ", hashlib.algorithms_guaranteed )
			exit( 1 )
		if hashmethod in unsafeMethods:
			print ( "Fatal: Methode (2. Argument) ist Liste unsicherer Methoden ", unsafeMethods )
			exit( 2 )
		
		if rounds < 10:
			rounds=10**rounds
		
		if rounds < minRounds:
			print ( "Fatal: Es ist keine gute Idee, den Hashvorgang weniger als " + str(minRounds)  + " mal durchzuführen." )
			exit( 4 )				
		if saltLen < minSaltLen:
			print ( "Fatal: Es ist keine gute Idee, eine Saltlänge unter " + str(minSaltLen)  + " zu benutzen." )
			exit( 4 )	
		
		chars='0123456789qwertzuiopasdfghjklyxcvbnmQWERTZUIOPASDFGHJKLYXCVBNM./'
		salt=''
		for i in range(saltLen):
			pos = random.randrange( len( chars ) )
			salt += chars[pos]
		
		hash = hashlib.pbkdf2_hmac(
			hashmethod,
			bytes(password,	encoding='utf-8'),
			bytes(salt,     encoding='utf-8'),
			int(rounds)
		)
		hash = str(base64.b64encode( hash ))[2:-1]
		hash=re.sub( '=+$', '', hash )
		return ( 
			'pbkdf2_hmac' 
			+ '_' 
			+ hashmethod 
			+ '_' 
			+ str(rounds) 
			+ '$' 
			+ salt
			+ '$'
			+ (hash)
		)

	def verify ( password, pwhash ):
		mod, salt, hash = pwhash.split('$')
		#print(mod.split('_')); exit()
		system1, system2, hashmethod,rounds = mod.split('_')
		if not hashmethod in hashlib.algorithms_available:
			print ( "Fatal: Methode '" + hashmethod + "' ist nicht in ", hashlib.algorithms_guaranteed )
		#testhash = hashlib.pbkdf2_hmac( hashmethod, bytes(password, encoding='utf-8'), bytes( salt, encoding='utf-8'), int(rounds) )
		testhash = hashlib.pbkdf2_hmac(
			hashmethod,
			bytes(password,	encoding='utf-8'),
			bytes(salt,     encoding='utf-8'),
			int(rounds )
		)
		testhash = str(base64.b64encode( testhash ))[2:-1]
		testhash=re.sub( '=+$', '', testhash )
		
		testhash =  str(
			mod
			+ '$' 
			+ salt
			+ '$'
			+ testhash
		)
		#print('hash:',pwhash)
		#print('test:',testhash)
		return testhash == pwhash

class php_password:
	def hash( password ):
		s = str (
			bcrypt.hashpw( bytes(password, 'utf-8'), bcrypt.gensalt() )
		)
		return s[2:-1]

	def verify( password, hash ):
		return bcrypt.checkpw( bytes(password, 'utf-8'), bytes(hash, 'ascii') )



print ("\n####################\nHashlib-Hashes\n####################")
hash = password.hash( 'hallo', 'sha512' )
print( hash )
if password.verify( 'hallo1', hash ):
	print('Hallo Boris: Du bist drin')
else:
	print('Nö')

print ("\n####################\nPHP-Hashes\n####################")

hash = php_password.hash('hallo')
print( hash )
if php_password.verify( 'hallo1', hash ):
	print('Hallo Boris: Auch mit PHP bist Du drin')
else:
	print('Nö')
exit(0)
  1. Hallo Raketenwilli,

    kannstenich ein kleines PHP Script schreiben und PHP im Hintergrund aufrufen?

    Rolf

    --
    sumpsi - posui - obstruxi
    1. kannstenich ein kleines PHP Script schreiben und PHP im Hintergrund aufrufen?

      Das gänge wohl, dann müsste ich aber dafür sorgen, dass PHP und Python verschlüsselt miteinander kommunizieren, denn das Durchreichen des Passwortes im Klartest via Parameter, Fifo oder Pipe dürfte als unsicher moniert werden. Ich teste in den nächsten Tagen mal, was passiert, wenn ich andere Algos verwende.

      Teufel, Fliegen und so weiter.

      1. Hallo Raketenwilli,

        okay, das kommt jetzt natürlich alles auf den Kontext an, in dem Du das bauen musst.

        Aber du könntest ja einen SSL gesicherten PHP Webservice schreiben…

        Spaß beiseite - ich habe gerade gefunden, dass $2b$ nicht unbedingt das Problem sein muss. Was steht dahinter? Eine 10 oder was anderes? Die verlinkte Seite sagt, dass das die Anzahl der gefahrenen bcrypt Salzrunden sei und PHP nur mit 10 klar käme. Kannst Du diese Rundenzahl auf Python-Seite beeinflussen?

        Nehm ich nach einer Verköstigungsprobe zurück, zumindest das aktuelle PHP verwendet 2y, nicht 2b.

        Neuer Versuch: Guck mal hier

        Rolf

        --
        sumpsi - posui - obstruxi
  2. Hallo,

    (und visa versa)

    Da du das hier schon häufiger im Forum verwendet hast: Woher hast du das eigentlich?
    (Denn gemeint ist natürlich „vice versa“.)

    Gruß
    Kalk

    1. Lieber Tabellenkalk,

      (und visa versa)

      Da du das hier schon häufiger im Forum verwendet hast: Woher hast du das eigentlich?
      (Denn gemeint ist natürlich „vice versa“.)

      Google Translator sagt, dass das Rumänisch sei.

      Liebe Grüße

      Felix Riesterer

      1. Hallo,

        (und visa versa)

        Da du das hier schon häufiger im Forum verwendet hast: Woher hast du das eigentlich?
        (Denn gemeint ist natürlich „vice versa“.)

        Google Translator sagt, dass das Rumänisch sei.

        das hätte ich jetzt nicht gedacht.
        Aber ich vermute auch, dass eher das aus dem Lateinischen stammende und im Englischen gebräuchliche vice versa gemeint ist.

        Einen schönen Tag noch
         Martin

        --
        "Was sind denn das für Beeren?" - "Das sind Blaubeeren." - "Warum sind sie dann rot?" - "Weil sie noch grün sind."
    2. Hallo,

      (und visa versa)

      Woher hast du das eigentlich?

      Das ist „slang“. Rumänisch ist, verglichen mit Latein, übrigens auch sowas wie sächseln. Also an der Ursprache (Mitteldeutsch) näher dran als die modernen Sprachen, wie das teutogermanische… oder eben am Schullatein, welches uns die Herren aus den Klöstern überbracht haben…

      1. Hallo Raketenwilli,

        wobei die Siebenbürgensachsen gar keine Sachsen sind.

        Die Autos von da unten heißen übrigens Dacia, weil die Romani, bevor sie domum eunt, die Provinz dort genau so nannten. Und das Latein ist dort geblieben. Die Bulgaren sind mit ihrem slawischen Kyrillisch nur durchgezogen.

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Hallo Rolf,

          wobei die Siebenbürgensachsen gar keine Sachsen sind.

          die Namen von historischen Völkern mit heutigen Regionen oder gar Bundesländern zu korrelieren, die "zufällig" ähnlich oder genauso heißen, geht meistens schief.

          Für viele Deutsche ist Schwaben ein Synonym für Baden-Württemberg. Aber z.B. die Augsburger (politisch heute in Bayern) legen großen Wert darauf, dass sie Schwaben sind und keine Bayern.
          Soweit ich weiß, ist das einstige Verbreitungsgebiet der Schwaben eher auf Ulm als auf Stuttgart zentriert, das heute als Herz vom Schwobaländle gilt.

          Die Autos von da unten heißen übrigens Dacia

          Danke für den Hinweis.
          Ich war bis eben überzeugt, Dacia sei ebenso wie Škoda eine tschechische Marke.

          Einen schönen Tag noch
           Martin

          --
          "Formschön, wetterfest, zweckfrei, und gegen Aufpreis auch entnehmbar."
          - Loriot, Familien-Original-Benutzer
  3. Moin,

    alternativ böte es sich eventuell an dein Programm in PHP statt Python zu entwickeln und PHP auf der Kommandozeile auszuführen.

    Viele Grüße
    Robert

  4. Mit der Methode ARGON2ID kann ich Passwörter in Python erzeugen und in PHP verifizieren oder in Python3 erzeugen und in PHP verifizieren.

    Statt Anleitung: Mein Test

    <?php
    /**
    * 
    * pip install argon2-cffi
    * Python3:
    * >>> from argon2 import PasswordHasher
    * >>> PasswordHasher.hash("correct horse battery staple")
    * '$argon2id$v=19$m=65536,t=3,p=4$FR1RjQI80l2wIZmZlIKQiQ$Rg7pZ/TtuInvJF31sr3BhvV47j10UeQIO01Rgp4P9OA'
    * 
    * >>> PasswordHasher.verify(
    * 	'$argon2id$v=19$m=65536,t=4,p=1$M0tXMmE4UVc1YzRwSmJEcQ$y/CeZ2oGrnj3ccgyFCZnl5oOqZilj6AI/5MK54HMDQQ',
    * 	'correct horse battery staple'
    * )
    * True
    */
    
    if ( 
    	password_verify(
    		'correct horse battery staple',
    		'$argon2id$v=19$m=65536,t=3,p=4$FR1RjQI80l2wIZmZlIKQiQ$Rg7pZ/TtuInvJF31sr3BhvV47j10UeQIO01Rgp4P9OA'
    	)
    ) {
    	echo "Ja";
    } else {
    	echo "Nein";
    }
    
    echo PHP_EOL;
    
    echo password_hash('correct horse battery staple', PASSWORD_ARGON2ID);
    
    1. Mit der Methode ARGON2ID kann ich Passwörter in Python erzeugen und in PHP verifizieren oder in Python3 erzeugen und in PHP verifizieren.

      Statt Anleitung: Mein Test

      Hm. Beim Python-Teil hab ich von mir selbst falsch abgeschrieben... Es fehlen Klammern hinter PasswordHasher.

      Dazu kommt dann noch: Die PHP-Funktion

      password_verify($hash, PASSWORD_ARGON2ID )
      

      liefert für die so erzeugten Hashes ein true, was andeutet das diese zu schwach sind.

      Mist!

      Aber in den Tiefen des Handbuchs der wohl von Support lebenden Hersteller von argon2-cffi (Version jetzt 21.3.0) fand ich dann den Hinweis, dass ich deren argon2.PasswordHasher() anders initialisieren kann…

      Mit

      Python:

      # pip install argon2-cffi nicht vergessen!
      
      import argon2
      
      time_cost   = 4       #Default wäre 3, mehr ist stärker
      memory_cost = 65536   #mehr wäre stärke
      parallelism = 1       #Default wäre 4, weniger ist auf der eigenen Maschine
                            # „stärker“ (verlangsamt die Erzeugung/Verifizierung)
                            #(Hacker beachten das aber nicht, wenn diese mit
                            #eigenen Programmen erbeutete Hash knacken wollen)
      hash_len    = 32      #mehr (→längerer Hash) wäre stärker, 64 funktioniert…
      salt_len    = 22      #Default wäre 16, andere sehen 22 als besser an
      encoding    = 'utf-8'
      
      passwordHasher = argon2.PasswordHasher(
          time_cost,
          memory_cost,
          parallelism,
          hash_len,
          salt_len,
          encoding
      )
      
      passwordHasher.hash("correct horse battery staple")
      

      wird dann ein hash generiert, den dann in PHP die Funktion password_verify($hash, PASSWORD_ARGON2ID ) akzeptiert, also false liefert.

      Dieses Problem ist also erst einmal gelöst. PHP kann man übrigens nach Konstanten fragen:

      PASSWORD_ARGON2_DEFAULT_MEMORY_COST	«65536»
      PASSWORD_ARGON2_DEFAULT_TIME_COST	«4»
      PASSWORD_ARGON2_DEFAULT_THREADS	«1»
      …
      

      Da fehlt nur die Länge des Salts.