Prompted by one of the reviewers of my upcoming book I decided I needed a simple, thread safe, and reasonably secure password store backed by SQLite.
The design criteria were straight forward and based in part on Storing passwords - done right! and the practical recommendations onPythonSecurity.org:
- based on SQLite
- allow for a reasonable amount of threads (its intended use is within a CherryPy application)
- able to use a salt with a configurable number of random bits
- able to apply key stretching with a configurable number of iterations
- use any secure hash algorithm from Python's hashlib module
Example
- from dbpassword import dbpassword
- dbpw = dbpassword('/var/password.db')
- # later, from any thread
- dbpw.update(user,plaintextpassword) # update or set a new password
- if dbpw.check(user,plaintextpassword) :
- ... do stuff ...
- else:
- ... warn off user ...
The dbpassword module
Warning! I am not a cryptographer so I cannot guarantee the following code is safe enough for your needs.
- '''''
- dbpassword.py Copyright 2011, Michel J. Anders
- This program is free software: you can redistribute it
- and/or modify it under the terms of the GNU General Public
- License as published by the Free Software Foundation,
- either version 3 of the License, or (at your option) any
- later version.
- This program is distributed in the hope that it will be
- useful, but WITHOUT ANY WARRANTY; without even the implied
- warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
- PURPOSE. See the GNU General Public License for more
- details.
- You should have received a copy of the GNU General Public
- License along with this program. If not, see
- <http: www.gnu.org="" licenses="">.
- '''
- import sqlite3
- import hashlib
- from random import SystemRandom as sr
- import threading
- class dbpassword:
- @staticmethod
- def hashpassword(name,salt,plaintextpassword,n=10):
- if n<1 : raise ValueError("n < 1")
- d = hashlib.new(name,(salt+plaintextpassword).encode()).digest()
- while n:
- n -= 1
- d = hashlib.new(name,d).digest()
- return hashlib.new(name,d).hexdigest()
- @staticmethod
- def getsalt(randombits=64):
- if randombits<16 : raise ValueError("randombits < 16")
- return "%016x"%sr().getrandbits(randombits)
- def __connect(self):
- if not hasattr(self.local,'con') or self.local.con is None:
- self.local.con = sqlite3.connect(self.db)
- self.local.con.create_function('crypt',2,
- lambda s,p:dbpassword.hashpassword(
- self.secure_hash,s,p,self.iterations))
- return self.local.con
- def __init__(self,db,
- secure_hash='sha256',iterations=1000,saltbits=64):
- self.db = db
- self.local = threading.local()
- self.secure_hash = secure_hash
- self.iterations = iterations
- self.saltbits = 64
- with self.__connect() as con:
- cursor=con.cursor()
- sql='create table if not exists pwdb (user unique, salt, password)'
- cursor.execute(sql)
- def update(self,user,plaintextpassword):
- with self.__connect() as con:
- cursor=con.cursor()
- sql1='insert or replace into pwdb (user,salt) values(?,?)'
- sql2='update pwdb set password=? where user = ?'
- salt=dbpassword.getsalt(self.saltbits)
- cursor.execute(sql1,(user,salt))
- cursor.execute(sql2,(dbpassword.hashpassword(
- self.secure_hash,salt,plaintextpassword,
- self.iterations),user))
- def check(self,user,plaintextpassword):
- cursor=self.__connect().cursor()
- sql='select user from pwdb where user = ? and crypt(salt,?) = password'
- cursor.execute(sql,(user,plaintextpassword))
- found=list(cursor) # can only create a list form this iterator once!
- return len(found)==1
- </http:>