Python Password Manager – Code Review Stack Exchange

I write a password manager in python and I know that the storage of passwords is very thorough (do not worry, mine are not plain text). I hoped that this community could help me improve the style, the use of libraries or anything else. All pointers are accepted with pleasure.

I've implemented some ideas here:

  • encrypt each password with a single salt, even in memory
  • encrypt each database with a single salt when they are stored long-term
  • be able to save to a database file (custom format)
  • be able to read from a database file (custom format)

I know that many services are already doing these things, but I thought I could try, learn, and have fun. Some examples of using the library are provided by the rider file.

runner:

import system, bones
from .passdb import PassDB

if __name__ == "__main__":
a = PassDB ()
# print (a)
a.password = "password"
a.set_entry ("user", "localhost", "sample_password")
# print (a.enc_str ())
a_copy = PassDB.open_db (a.enc_str (), "password")
# print (a_copy.password)
if a_copy is not None:
print (a_copy.get_entry ("user @ localhost"))
print (a_copy.get_password ("user @ localhost"))
a_copy.save_as ("tmp.passdb", "sample password")

passdb.py:

import into base64
import hashlib
pandas of import
since import crypto Random
since Crypto.Cipher import AES
import json
import re
of import StringIO
import date / time


PassDB class (object):

_valid_init_fields = ["data", "path", "password", "settings"]
    version = "Version 0.0.1"
parameters: dict
data: pandas.DataFrame
_defaults = {
"salt_size": 64,
"block_size": 32, # Using AES256
"enc_sample_content": "The password provided is correct",
"salt": none,
"path": none,
"hash_depth": 9
}

_format = "" "### PYPASSMAN {version} ###
{Settings}
### SAMPLE ###
{} Enc_sample
### THE DATA ###
{The data}
"" "

def __init __ (self, * args, ** kwargs):
if len (args)> 3:
raises TypeError ("Too many arguments")
if len (args)> 2:
self.data = args[2]
        other:
self.data = None
if len (args)> 1:
self.password = args[1]
        other:
self.password = None
if len (args)> 0:
self.path = args[0]
        other:
self.path = None

for the key, arg in kwargs.items ():
if you enter self._valid_init_fields:
setattr (self, key, arg)

if self.data is None:
self.data = pandas.DataFrame (
columns =[
                    "account",
                    "hostname",
                    "salt",
                    "password",
                    "hash_depth",
                    "dateModified",
                    "dateCreated"
                    ]
                )

if getattr (self, "settings", None) is None:
self.settings = self._defaults.copy ()
if self.settings.get ("salt", None) is None:
auto.settings["salt"] = base64.b64encode (Random.new (). read (
auto.settings["salt_size"]
            )). decode ("utf-8")

for the key in self._defaults.keys ():
if the key is not in self.settings:
auto.settings[key] = self._defaults[key]

    @classmethod
def open_db (cls, raw, password):
parameters, sample, data = (* map (
lambda string: string.strip (),
re.split (r "###. * ###  n", raw)[1:]
            )
settings = json.loads (parameters)
sample = cls._decrypt (example, password, parameters)["salt"], settings["hash_depth"])
otherwise sample == parameters["enc_sample_content"]:
raises ValueError (
"Can not open PassDB: incorrect password provided")
data = cls._decrypt (data, password, settings["salt"], settings["hash_depth"])
data = pandas.read_csv (StringIO (data))
output = cls (
parameters = parameters,
data = data,
password = password
)
return from exit

def save_as (auto, path, password):
settings_cp = self.settings.copy ()
settings_cp["path"] = path
new_dict = self .__ class __ (
data = self.data,
path = path,
password = password,
parameters = parameters_cp
)
new_dict.save ()
returns True

def save (auto):
with open (self.path, "w +") as destination:
enc_data = self._encrypt (
self.data.to_csv (index_label = "index"),
self.password, self.settings["salt"],
auto.settings["hash_depth"]
            )
enc_sample = self._encrypt (
auto.settings["enc_sample_content"],
self.password, self.settings["salt"],
auto.settings["hash_depth"])
dest.write (self._format.format (
version = str (self.version),
settings = json.dumps (self.settings),
data = enc_data,
enc_sample = enc_sample
))

@classmethod
def _encrypt (cls, raw, password, salt, hash_depth):
raw = cls._pad (raw)
iv = Random.new (). read (AES.block_size)
salt = base64.b64decode (salt)
key = hashlib.sha256 (
str (password) .encode () + salt
).digest()
for i in the beach (hash_depth):
key = hashlib.sha256 (key + salt) .digest ()
cipher = AES.new (key, AES.MODE_CBC, iv)
returns base64.b64encode (iv + cipher.encrypt (raw)). decode ("utf-8")

@classmethod
def _decrypt (keys, enc, password, salt, hash_depth):
enc = base64.b64decode (enc)
iv = enc[:AES.block_size]
        salt = base64.b64decode (salt)
key = hashlib.sha256 (
password.encode () + salt
).digest()
for i in the beach (hash_depth):
key = hashlib.sha256 (key + salt) .digest ()

cipher = AES.new (key, AES.MODE_CBC, iv)
try:
returns cls._unpad (
cipher.decrypt (
enc[AES.block_size:]
                )
) .decode (& # 39; utf-8 & # 39;)
except UnicodeDecodeError:
generate ValueError ("incorrect password")

@classmethod
def _pad (cls, s):
bs = cls._defaults["block_size"]
        return (
s + (bs - len (s)% bs) *
chr (bs - len (s)% bs)
)

@staticmethod
def _unpad (s):
results[:-ord(s[len(s)-1:])]def enc_str (auto):
enc_data = self._encrypt (
self.data.to_csv (index_label = "index"),
self.password, self.settings["salt"],
auto.settings["hash_depth"]
            )
enc_sample = self._encrypt (
auto.settings["enc_sample_content"],
self.password, self.settings["salt"],
auto.settings["hash_depth"]
            )
return (self._format.format (
version = str (self.version),
enc_sample = enc_sample,
settings = json.dumps (self.settings),
data = enc_data
))

def __str __ (auto):
path = self.settings["path"]
        return "PassDB <{} entries{}>".format(
len (self.data),
"at & # 39; {} & # 39;" format (path) if the path is not any other ""
)

def set_entry (self, * args):
account, host name, password = None, None, None
if len (args) == 1:
account, hostname_password = args[0].Split("@")
hostname, password, other = hostname_password.split (":")
elif len (args) == 2:
account_hostname, password = arguments
account, hostname = account_hostname.split ("@")
elif len (args) == 3:
account, hostname, password = args
other:
raises ValueError ("" "
PassDB.set_entry :: Too many arguments
usage (1): get_password (account, host name, password)
usage (2): get_password ("{account} @ {hostname}", password)
usage (3): get_password ("{account} @ {hostname}: {password}") "" "
)

for char in (":", "@"):
for the item in the account, host name, password:
if character in object:
raises ValueError ("" "
account, hostname and password can not contain colon (:) or symbol (@) "" ")

if len (self.data)> 0:
for index, entry in self.data.iterrows ():
if entry["account"] == account and registration["hostname"] == hostname:
salt = base64.b64encode (Random.new (). read (
auto.settings["salt_size"]
                    )). decode ("utf-8")
password = self._encrypt (
password,
auto.settings["salt"],
salt,
auto.settings["hash_depth"]
                        )
self.data.loc[index] = (
account, host name,
salt, password,
auto.settings["hash_depth"],
str (datetime.datetime.utcnow (). isoformat ()),
str (datetime.datetime.utcnow (). isoformat ())
)
other:
salt = base64.b64encode (Random.new (). read (
auto.settings["salt_size"]
            )). decode ("utf-8")
password = self._encrypt (
password,
auto.settings["salt"],
salt,
auto.settings["hash_depth"]
                )
self.data.loc[0] = (
Account,
host name,
salt,
password,
auto.settings["hash_depth"],
str (datetime.datetime.utcnow (). isoformat ()),
str (datetime.datetime.utcnow (). isoformat ())
)

def get_entry (self, * args):
if len (args) == 1:
account, host name = args[0].Split("@")
elif len (args) == 2:
account, host name = args
other:
raises ValueError ("" "
PassDB.get_entry :: Too many arguments
usage (1): get_entry (account, hostname)
usage (2): get_entry ("{account} @ {hostname}") "" ")
if (getattr (self, "password") is None):
raise ValueError ("Can not get an entry when PassDB instance password is None")
if (len (self.data)) == 0:
return None
for index, entry in self.data.iterrows ():
if entry["account"] == account and registration["hostname"] == hostname:
return entry
return None

def get_password (self, * args):
if len (args) == 1:
account, host name = args[0].Split("@")
elif len (args) == 2:
account, host name = args
other:
raises ValueError ("" "
PassDB.get_password :: Too many arguments
usage (1): get_password (account, hostname)
usage (2): get_password ("{account} @ {hostname}") "" ")

entry = self.get_entry (account, hostname)
if isinstance["password"], str):
return self._decrypt (input["password"], auto.settings["salt"]Entrance["salt"]Entrance["hash_depth"])
raise ValueError ("Password for {account} @ {host name} in unexpected format" .format (enter **))
`` `