I am starting my adventure with Python and object oriented programming.
I wrote a script for salting and hashing password. My main goal is practice with OOP, not cryptography. I know there is no error handling.
Wonder how improve my code it and make more object oriented. It would be nice if someone more experienced can take a look and give me some advice how to make it better. Any help for newbie would be great!
Description:
The script is not really useful in real life.
1st part:
Hash and salt password and next encode to base64 with added salt (needed for future comparing hashes). Algorithm: base64( SHA256(password + salt) + salt)
Salt is X random bytes where X can be specify but by default it is 16.
2nd part (some kind of authorization):
Compare input hash (e.x. from database) with new created hash from input plain password.
New salt is created from salt taken from input hash.
Code:
import base64
import hashlib
import os
class Hashing(object):
# base64( SHA256(password + salt) + salt)
# generate new salt (default 16 bytes)
def generate_new_salt(self, salt_ln=16):
self.new_salt = os.urandom(salt_ln)
print(f'new salt: {self.new_salt}')
return self.new_salt
# get salt from hash
def get_old_salt(self, input_hash, salt_ln=16):
self.old_salt = base64.b64decode(input_hash)[-salt_ln:]
print(f'old salt: {self.old_salt}')
return self.old_salt
# compute hash using parameters
def compute_hash(self, password, salt):
self.salt = salt
self.enc_password = password.encode()
# hashing SHA256(password + salt)
hash_object = hashlib.sha256(self.enc_password + salt)
# add salt to hash and encode to base64
hash_b64 = base64.b64encode(hash_object.digest() + salt)
print(f'new_hash: {hash_b64}')
return hash_b64
# create hash from new or old salt
def create_hash(self, password, salt_ln=16,old_salt=None):
if old_salt: #if old salt then use it
self.salt = old_salt
else: #else generate new salt
self.salt = Hashing().generate_new_salt(salt_ln)
hash = Hashing().compute_hash(password, self.salt)
return hash
# compare input hash with created using salt get from input
def compare_hashes(self, password, old_hash, salt_ln=16):
self.enc_password = password.encode()
#get salt from input hash
self.old_salt = Hashing().get_old_salt(old_hash, salt_ln)
#recreat input hash
re_hash = Hashing().create_hash(password,salt_ln, self.old_salt)
print(f'Compare: old_hash: {old_hash}')
print(f'Compare: new_hash: {re_hash}')
#compare
if re_hash.decode() == old_hash.decode():
return True
else:
return False
#code below is just for testing
NewSalt = Hashing().generate_new_salt()
Hash = Hashing().create_hash('pass')
OldSalt = Hashing().get_old_salt(Hash)
CompareHash = Hashing().compare_hashes('pass', Hash)
if CompareHash:
print('HASHES THE SAME')
else:
print('NOT THE SAME')
print(CompareHash)
Questions:
What is a real difference between these two? With and w/o self.X works
def generate_new_salt(self, salt_ln=16): self.new_salt = os.urandom(salt_ln) print(f'new salt: {self.new_salt}') return self.new_salt
def generate_new_salt(self, salt_ln=16): new_salt = os.urandom(salt_ln) print(f'new salt: {new_salt}') return new_salt
I have problem with passing default value of salt length,
salt_ln = 16
. For me it does not look nice when it is replicated in every method. Is there any way to do it more global?
-
3\$\begingroup\$ Note that sha256 is not secure for passwords: security.stackexchange.com/questions/90064/… \$\endgroup\$markspace– markspace2020年01月28日 23:04:20 +00:00Commented Jan 28, 2020 at 23:04
-
\$\begingroup\$ @markspace this script is just exercice with OOP, not cryptography. Ps. Fixed bug in code \$\endgroup\$kedulyw– kedulyw2020年01月28日 23:43:35 +00:00Commented Jan 28, 2020 at 23:43
1 Answer 1
Questions
self
is the instance of whatever class the method is defined on. In your example the method isgenerate_new_salt
which is defined on theHashing
class.When you use
my_hashing = Hashing()
you're creating an instance and binding it to the variablemy_hashing
. When you then call the method, via something likemy_hashing.generate_new_salt(...)
thenself
is the same asmy_hashing
. They both point to the same instance ofHashing
.>>> class Foo: def comp(self): return my_foo == self >>> my_foo = Foo() >>> my_foo.comp() True
This means that
self.foo = ...
binds...
to the instance. This allows you to then useself.foo
to get the value,...
, back at a later date. Without having to pass it as an argument. If you have no reason to useself.foo
rather than justfoo
, then usingself.foo
is not a good idea.This comes from two different but linked things.
You're trying to be too helpful. A lot of your code is not doing much, and is just calling other functions.
This is a common problem when programming, and so I would recommend you follow the KISS principle. If you don't need the helper now, then you don't need it at all.
You're not really using objects to their potential. If you wrap all of your methods in
staticmethod
decorators then nothing will change. This highlights that you're not really following OOP. And it highlights you're barely using the featuresclass
provides.
Given the above points, it would be best to start over.
Review
It's normally recommended to not use
print
. There are exceptions, but in most cases you're better served either by just not havingprint
or usinglogging
.There is one exception, which is to interact with the user via the CLI. And in this case
Hashing
does not need to do that. Where you're testing needs it and so you should only have them below that comment "code below is just for testing".Inheriting from object,
Hashing(object)
, is redundant in Python 3.Rather than using an
if
to compare something and thenreturn
True
andFalse
. You can just return the comparison.return re_hash.decode() == old_hash.decode()
It would be best if the code below the comment "code below is just for testing" were in a function. This would mean that the global namespace isn't polluted with things that don't need to be there.
- It's best to use an
if __name__ == '__main__':
guard. Since you only run the main function if you're testing, this will allow you to import the code without the testing code running.
From the ground up
Merging both the hash and the salt into the encoding, "base64(hash + salt)", is a smart way to keep the hash and the salt together. However it would be clearer if you create a class that stores the hash and the salt independently.
class PasswordHash: def __init__(self, hash, salt): self.hash = hash self.salt = salt ph = PasswordHash(b'hash', b'salt') print(ph.hash) # b'hash' print(ph.salt) # b'salt'
From here you should be able to see two things:
- The method
get_old_salt
is now obsolete. - You no longer need to care about the salt length.
- The method
When you first define a class, after writing the
__init__
, you should ask yourself if the class will ever be printed. If it could be printed, then you should make at least one of these dunder methods, double underscore method.__repr__
- Instructions on how to rebuild the method.__str__
- A human friendly interpretation of the class.
In this case
__repr__
could make sense, but__str__
doesn't.def __repr__(self): return f'PasswordHash({self.hash}, {self.salt})'
You should be able to see,
__repr__
outputs the exact text we used to make the instance.>>> print(PasswordHash(b'hash', b'salt')) PasswordHash(b'hash', b'salt')
From here we need to think about how we should hash the password, since we don't want to store the password in plaintext. And so we can add
compute_hash
toPasswordHash
, with 2 modifications.- I prefer the name
hash_password
, as it explains what it does, and what it does it on. Secondly I would make it a
staticmethod
. This comes with a couple of reasons why:- It allows us to create
PasswordHash
from a password easier. - It allows for subclasses of
PasswordHash
to change the way the class hashes things.
- It allows us to create
@staticmethod def hash_password(password, salt): hash = hashlib.sha256(password.encode() + salt) return base64.b64encode(hash.digest())
Currently making a
PasswordHash
from a password is pretty ugly.ph = PasswordHash(PasswordHash.hash_password(password, salt), salt)
- I prefer the name
Since the current interface for making a
PasswordHash
from a password is not the nicest. And is the standard way to use the class, we can make a class method to make it pretty.The reason this is a
classmethod
and not a standard method is because we need it to run before__init__
. It is also aclassmethod
and not astaticmethod
as we'll be using the first argument,cls
, to instantiate the class.@classmethod def from_password(cls, password, salt): return cls(cls.hash_password(password, salt), salt)
Now making the
PasswordHash
from a password is really clean.ph = PasswordHash.from_password('pass', salt)
We're almost at the end. The last thing to do is to add the spiritual child of
compare_hashes
to the class. Which is really simple since we haveself.salt
andPasswordHash.hash_password
.def compare(self, password): return self.hash == self.hash_password(password, self.salt)
Tying this all together you can get:
import base64
import hashlib
import os
def new_salt(length=16):
return os.urandom(length)
class PasswordHash:
def __init__(self, hash, salt):
self.hash = hash
self.salt = salt
@classmethod
def from_password(cls, password, salt):
return cls(cls.hash_password(password, salt), salt)
def __repr__(self):
return f'PasswordHash({self.hash}, {self.salt})'
@staticmethod
def hash_password(password, salt):
hash = hashlib.sha256(password.encode() + salt)
return base64.b64encode(hash.digest())
def compare(self, password):
return self.hash == self.hash_password(password, self.salt)
def main():
salt = new_salt()
print('salt:', salt)
hash = PasswordHash.from_password('pass', salt)
print('PasswordHash:', hash)
print('hash:', hash.hash)
print('salt:', hash.salt)
print('compare pass:', hash.compare('pass'))
print('compare bar :', hash.compare('bar'))
if __name__ == '__main__':
main()
To make the comparison to be more OOP you could rename compare
to the dunder method __eq__
. And then you could use it via the ==
operator.
print('compare pass:', hash == 'pass')
print('compare bar :', hash == 'bar')
Explore related questions
See similar questions with these tags.