diff --git a/README.md b/README.md
index 2a976ba..60873a8 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# keepassc and python-keepass
-This provides command line and Python interfaces for operating on
+This provides command line and Python (both 2 and 3) interfaces for operating on
files in KeePass format v3 (used by [KeePass](http://keepass.info/)
1.x, and [KeePassX](http://www.keepassx.org/)). Note, this is not the
format used by the KeePass application version 2.x.
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..60873a8
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,106 @@
+# keepassc and python-keepass
+
+This provides command line and Python (both 2 and 3) interfaces for operating on
+files in KeePass format v3 (used by [KeePass](http://keepass.info/)
+1.x, and [KeePassX](http://www.keepassx.org/)). Note, this is not the
+format used by the KeePass application version 2.x.
+
+## Notes of caution
+
+Before using this code, understand the its (known) security
+and correctness limitations:
+
+ * Unlike the KeePass/KeePassX GUI applications this code makes no
+ attempt to secure its memory. Input files read in are stored in
+ memory fully decrypted.
+
+ * It is quite easy to display the stored passwords in plain text,
+ although the defaults try to avoid this.
+
+ * Specifying the master key on the command line will leave traces in
+ your shells history and in the process list.
+
+ * While input files are treated as read-only, keep backups of any
+ files written by KeePass/KeePassX until you are assured that files
+ written by this code are usable.
+
+ * Key files are not currently supported.
+
+## Prerequisites and Installation
+
+You will need to install the python-crypto package (providing the
+"Crypto" module). On a well behaved system do:
+
+```shell
+sudo apt-get install python-crypto
+```
+
+If installing into a [virtualenv](http://www.virtualenv.org) this prerequisite will be installed for you:
+
+```shell
+virtualenv /path/to/venv
+source /path/to/venv/bin/activate
+cd python-keepass
+python setup.py install
+```
+
+
+## Command line
+
+The command line interface is run like:
+
+```shell
+keepassc [general_options] [command command_options] ...
+```
+
+Multiple commands can be specified and will be executed in order.
+They operate on an in-memory instance of the database file. An
+example,
+
+```shell
+keepass open -m secret file.kdb \
+ dump -p -f '%(username)s password is: %(password)s' \
+ save -m newsecret backup.kdb
+```
+
+Online help:
+
+```shell
+keepass -h # short usage
+keepass help # full usage
+```
+
+## Python Modules
+
+### Low level file access
+
+```python
+from keepass import kpdb
+db = kpdb.Database(filename,masterkey)
+print db # warning: displayed passwords in plaintext!
+```
+
+# References and Credits
+
+## PyCrypto help
+
+ * Main page is found through . The documentation there is a start, but not enough.
+ * This blog post is useful for the basics:
+
+## The giants on whose shoulders this works stands
+
+First, thanks to the original authors, contributors and community
+behind KeePass and KeePassX. I am merely a user of KeePassX.
+
+A big credit is due to rudi & shirou (same hacker?) for the following:
+
+ *
+ *
+
+Looking through KeePass/KeePassX source made my head swim. Only after
+reviewing their work could I get started.
+
+## License
+
+This package is Free Software licensed to you under the GPL v2 or
+later at your discretion. See the [LICENSE.txt](LICENSE.txt) file for details.
diff --git a/python/keepass/__init__.py b/python/keepass/__init__.py
index 9fd3e88..75cc2ba 100644
--- a/python/keepass/__init__.py
+++ b/python/keepass/__init__.py
@@ -12,3 +12,14 @@
# ... even thoough this file is essentially empty....
+__author__ = ["Brett Viren", "Jeremy Ehrhardt", "Filipp Frizzy"]
+__copyright__ = ""
+__credits__ = ["Brett Viren", "Jeremy Ehrhardt", "Filipp Frizzy"]
+__license__ = "GPLv2+"
+__version__ = "1.2"
+__maintainer__ = "Filipp Frizzy"
+__email__ = "filipp.s.frizzy@gmail.com"
+__status__ = "Development"
+__description__ = "Python (both 2 and 3) interface and cli to KeePass file format v3 (used in KeePass V1.x and KeePassX)"
+__keywords__ = "python keepass kdb"
+__url__ = "https://github.com/Friz-zy/python-keepass"
diff --git a/python/keepass/cli.py b/python/keepass/cli.py
index bdfb022..de6a013 100644
--- a/python/keepass/cli.py
+++ b/python/keepass/cli.py
@@ -11,6 +11,10 @@
# later version.
import sys
+import six
+from optparse import OptionParser
+import getpass
+from keepass import kpdb
class Cli(object):
'''
@@ -49,7 +53,7 @@ def splitopts(argv):
cmd=""
if argv[0][0] != '-':
if argv[0] not in Cli.commands:
- raise ValueError,'Unknown command: "%s"'%argv[0]
+ raise ValueError('Unknown command: "%s"'%argv[0])
cmd = argv.pop(0)
pass
copy = list(argv)
@@ -79,7 +83,7 @@ def splitopts(argv):
def __call__(self):
'Process commands'
if not self.command_line:
- print self._general_op().print_help()
+ six.print_((self._general_op().print_help()))
return
for cmd,cmdopts in self.command_line:
meth = eval('self._%s'%cmd)
@@ -99,7 +103,6 @@ def _general_op(self):
execute "help" command for more information.
'''
- from optparse import OptionParser
op = OptionParser(usage=self._general_op.__doc__)
return op
@@ -114,25 +117,24 @@ def _help_op(self):
def _help(self,opts):
'Print some helpful information'
- print 'Available commands:'
+ six.print_(('Available commands:'))
for cmd in Cli.commands:
meth = eval('self._%s'%cmd)
- print '\t%s: %s'%(cmd,meth.__doc__)
+ six.print_(('\t%s: %s'%(cmd,meth.__doc__)))
continue
- print '\nPer-command help:\n'
+ six.print_(('\nPer-command help:\n'))
for cmd in Cli.commands:
meth = eval('self._%s_op'%cmd)
op = meth()
if not op: continue
- print '%s'%cmd.upper()
+ six.print_(('%s'%cmd.upper()))
op.print_help()
- print
+ six.print_(())
continue
def _open_op(self):
'open [options] filename'
- from optparse import OptionParser
op = OptionParser(usage=self._open_op.__doc__,add_help_option=False)
op.add_option('-m','--masterkey',type='string',default="",
help='Set master key for decrypting file, default: ""')
@@ -141,12 +143,11 @@ def _open_op(self):
def _open(self,opts):
'Read a file to the in-memory database'
opts,files = self.ops['open'].parse_args(opts)
- import kpdb
# fixme - add support for openning/merging multiple DBs!
try:
dbfile = files[0]
except IndexError:
- print "No database file specified"
+ six.print_(("No database file specified"))
sys.exit(1)
self.db = kpdb.Database(files[0],opts.masterkey)
self.hier = self.db.hierarchy()
@@ -154,7 +155,6 @@ def _open(self,opts):
def _save_op(self):
'save [options] filename'
- from optparse import OptionParser
op = OptionParser(usage=self._save_op.__doc__,add_help_option=False)
op.add_option('-m','--masterkey',type='string',default="",
help='Set master key for encrypting file, default: ""')
@@ -169,7 +169,6 @@ def _save(self,opts):
def _dump_op(self):
'dump [options] [name|/group/name]'
- from optparse import OptionParser
op = OptionParser(usage=self._dump_op.__doc__,add_help_option=False)
op.add_option('-p','--show-passwords',action='store_true',default=False,
help='Show passwords as plain text')
@@ -184,13 +183,12 @@ def _dump(self,opts):
if not self.hier:
sys.stderr.write('Can not dump. No database open.\n')
return
- print self.hier
+ six.print_((self.hier))
#self.hier.dump(opts.format,opts.show_passwords)
return
def _entry_op(self):
'entry [options] username [password]'
- from optparse import OptionParser
op = OptionParser(usage=self._entry_op.__doc__,add_help_option=False)
op.add_option('-p','--path',type='string',default='/',
help='Set folder path in which to store this entry')
@@ -208,7 +206,6 @@ def _entry_op(self):
def _entry(self,opts):
'Add an entry into the database'
- import getpass
opts,args = self.ops['entry'].parse_args(opts)
username = args[0]
try:
diff --git a/python/keepass/header.py b/python/keepass/header.py
index a488451..3337cd1 100644
--- a/python/keepass/header.py
+++ b/python/keepass/header.py
@@ -45,6 +45,8 @@
# later version.
import Crypto.Random
+import struct
+import six
class DBHDR(object):
'''
@@ -52,17 +54,17 @@ class DBHDR(object):
'''
format = [
- ('signature1',4,'I'),
- ('signature2',4,'I'),
- ('flags',4,'I'),
- ('version',4,'I'),
- ('master_seed',16,'16s'),
- ('encryption_iv',16,'16s'),
- ('ngroups',4,'I'),
- ('nentries',4,'I'),
- ('contents_hash',32,'32s'),
- ('master_seed2',32,'32s'),
- ('key_enc_rounds',4,'I'),
+ ('signature1',4,b'I'),
+ ('signature2',4,b'I'),
+ ('flags',4,b'I'),
+ ('version',4,b'I'),
+ ('master_seed',16,b'16s'),
+ ('encryption_iv',16,b'16s'),
+ ('ngroups',4,b'I'),
+ ('nentries',4,b'I'),
+ ('contents_hash',32,b'32s'),
+ ('master_seed2',32,b'32s'),
+ ('key_enc_rounds',4,b'I'),
]
signatures = (0x9AA2D903,0xB54BFB65)
@@ -86,6 +88,9 @@ def __init__(self,buf=None):
self.version = 0x30002
self.flags = 3 # SHA2 hashing, AES encryption
self.key_enc_rounds = 50000
+ self.ngroups = 0
+ self.nentries = 0
+ self.contents_hash = ""
self.reset_random_fields()
def reset_random_fields(self):
@@ -112,29 +117,23 @@ def encryption_type(self):
def encode(self):
'Provide binary string representation'
- import struct
-
- ret = ""
-
+ ret = b""
for field in DBHDR.format:
name,bytes,typecode = field
value = self.__dict__[name]
- buf = struct.pack('<'+typecode,value)
+ buf = struct.pack(b'<'+typecode,value)
ret += buf
continue
return ret
def decode(self,buf):
'Fill self from binary string.'
- import struct
-
index = 0
-
for field in DBHDR.format:
name,nbytes,typecode = field
string = buf[index:index+nbytes]
index += nbytes
- value = struct.unpack('<'+typecode, string)[0]
+ value = struct.unpack(b'<'+typecode, string)[0]
self.__dict__[name] = value
continue
@@ -143,7 +142,7 @@ def decode(self,buf):
msg = 'Bad sigs:\n%s %s\n%s %s'%\
(DBHDR.signatures[0],DBHDR.signatures[1],
self.signature1,self.signature2)
- raise IOError,msg
+ raise IOError(msg)
return
diff --git a/python/keepass/hier.py b/python/keepass/hier.py
index 22f4d67..302a741 100644
--- a/python/keepass/hier.py
+++ b/python/keepass/hier.py
@@ -10,7 +10,8 @@
# Free Software Foundation; either version 2, or (at your option) any
# later version.
-import datetime
+from keepass.infoblock import GroupInfo
+import six
def path2list(path):
'''
@@ -62,10 +63,10 @@ def __call__(self,node):
class NodeDumper(Walker):
def __call__(self,node):
if not node.group:
- print 'Top'
+ six.print_(('Top'))
return None, False
- print ' '*node.level()*2,node.group.name(),node.group.groupid,\
- len(node.entries),len(node.nodes)
+ six.print_((' '*node.level()*2,node.group.name(),node.group.groupid,\
+ len(node.entries),len(node.nodes)))
return None, False
class FindGroupNode(object):
@@ -92,8 +93,6 @@ def __call__(self,node):
groupid = node.group.groupid
- from infoblock import GroupInfo
-
if top_name != obj_name:
return (None,True) # bail on the current node
@@ -124,7 +123,6 @@ def __init__(self):
def __call__(self,g_or_e):
if g_or_e is None: return (None,None)
- from infoblock import GroupInfo
if isinstance(g_or_e,GroupInfo):
self.groups.append(g_or_e)
else:
@@ -176,8 +174,6 @@ def __call__(self,g_or_e):
groupid = None
if g_or_e: groupid = g_or_e.groupid
- from infoblock import GroupInfo
-
if top_name != obj_name:
if isinstance(g_or_e,GroupInfo):
return (None,True) # bail on the current node
@@ -303,14 +299,13 @@ def walk(node,walker):
continue
return None
-def mkdir(top, path, gen_groupid):
+def mkdir(top, path, groupid, groups, header):
'''
Starting at given top node make nodes and groups to satisfy the
given path, where needed. Return the node holding the leaf group.
@param gen_groupid: Group ID factory from kpdb.Database instance.
'''
- import infoblock
path = path2list(path)
pathlen = len(path)
@@ -322,27 +317,8 @@ def mkdir(top, path, gen_groupid):
node = fg.best_match or top
pathlen -= len(fg.path)
for group_name in fg.path:
- # fixme, this should be moved into a new constructor
- new_group = infoblock.GroupInfo()
- new_group.groupid = gen_groupid()
- new_group.group_name = group_name
- new_group.imageid = 1
- new_group.creation_time = datetime.datetime.now()
- new_group.last_mod_time = datetime.datetime.now()
- new_group.last_acc_time = datetime.datetime.now()
- new_group.expiration_time = datetime.datetime(2999, 12, 28, 23, 59, 59) # KeePassX 0.4.3 default
- new_group.level = pathlen
- new_group.flags = 0
- new_group.order = [(1, 4),
- (2, len(new_group.group_name) + 1),
- (3, 5),
- (4, 5),
- (5, 5),
- (6, 5),
- (7, 4),
- (8, 2),
- (9, 4),
- (65535, 0)]
+ new_group = GroupInfo().make_group(group_name, pathlen, groupid)
+
pathlen += 1
new_node = Node(new_group)
@@ -350,6 +326,8 @@ def mkdir(top, path, gen_groupid):
node = new_node
group = new_group
+ groups.append(new_group)
+ header.ngroups += 1
continue
pass
return node
diff --git a/python/keepass/infoblock.py b/python/keepass/infoblock.py
index 451151f..11bd60e 100644
--- a/python/keepass/infoblock.py
+++ b/python/keepass/infoblock.py
@@ -13,6 +13,10 @@
import struct
import sys
+from datetime import datetime
+import uuid
+import six
+from binascii import b2a_hex, a2b_hex
# return tupleof (decode,encode) functions
@@ -20,12 +24,11 @@ def null_de(): return (lambda buf:None, lambda val:None)
def shunt_de(): return (lambda buf:buf, lambda val:val)
def ascii_de():
- from binascii import b2a_hex, a2b_hex
- return (lambda buf:b2a_hex(buf).replace('\0',''),
- lambda val:a2b_hex(val)+'\0')
+ return (lambda buf:b2a_hex(buf).replace(b'\0',b''),
+ lambda val:a2b_hex(val)+b'\0')
def string_de():
- return (lambda buf: buf.replace('\0',''), lambda val: val+'\0')
+ return (lambda buf: buf.replace(b'\0',b''), lambda val: val+b'\0')
def short_de():
return (lambda buf:struct.unpack("> 2);
@@ -69,7 +71,7 @@ def __init__(self,format,string=None):
def __str__(self):
ret = [self.__class__.__name__ + ':']
- for num,form in self.format.iteritems():
+ for num,form in six.iteritems(self.format):
try:
value = self.__dict__[form[0]]
except KeyError:
@@ -94,10 +96,19 @@ def decode(self,string):
if name is None: break
try:
value = decenc[0](buf)
- except struct.error,msg:
+ if name in ['group_name',
+ 'title',
+ 'url',
+ 'username',
+ 'password',
+ 'notes',
+ 'binary_desc',
+ 'uuid']:
+ value = value.decode()
+ except struct.error as msg:
msg = '%s, typ = %d[%d] -> %s buf = "%s"'%\
(msg,typ,siz,self.format[typ],buf)
- raise struct.error,msg
+ raise struct.error(msg)
self.__dict__[name] = value
continue
@@ -111,13 +122,17 @@ def __len__(self):
def encode(self):
'Return binary string representatoin'
- string = ""
+ string = b""
for typ,siz in self.order:
if typ == 0xFFFF: # end of block
encoded = None
else:
name,decenc = self.format[typ]
value = self.__dict__[name]
+ if six.PY3 and isinstance(value, six.string_types):
+ value = bytes(value, 'utf-8')
+ elif isinstance(value, six.string_types):
+ value = str(value)
encoded = decenc[1](value)
pass
buf = struct.pack(' 2147483446) or (not crypto_size and self.header.ngroups)):
- raise ValueError, "Decryption failed.\nThe key is wrong or the file is damaged"
+ raise ValueError("Decryption failed.\nThe key is wrong or the file is damaged")
- import hashlib
if self.header.contents_hash != hashlib.sha256(payload).digest():
- raise ValueError, "Decryption failed. The file checksum did not match."
+ raise ValueError("Decryption failed. The file checksum did not match.")
return payload
def decrypt_payload_aes_cbc(self, payload, finalkey, iv):
'Decrypt payload buffer with AES CBC'
-
- from Crypto.Cipher import AES
cipher = AES.new(finalkey, AES.MODE_CBC, iv)
payload = cipher.decrypt(payload)
- extra = ord(payload[-1])
+ if six.PY3:
+ extra = payload[-1]
+ else:
+ extra = ord(payload[-1])
payload = payload[:len(payload)-extra]
return payload
def encrypt_payload(self, payload, finalkey, enctype, iv):
'Encrypt payload'
if enctype != 'Rijndael':
- raise ValueError, 'Unsupported encryption type: "%s"'%enctype
+ raise ValueError('Unsupported encryption type: "%s"'%enctype)
return self.encrypt_payload_aes_cbc(payload, finalkey, iv)
def encrypt_payload_aes_cbc(self, payload, finalkey, iv):
'Encrypt payload buffer with AES CBC'
- from Crypto.Cipher import AES
cipher = AES.new(finalkey, AES.MODE_CBC, iv)
# pad out and store amount as last value
length = len(payload)
- encsize = (length/AES.block_size+1)*16
- padding = encsize - length
+ encsize = (length//AES.block_size+1)*16
+ padding = int(encsize - length)
for ind in range(padding):
- payload += chr(padding)
+ payload += chr(padding).encode('ascii')
return cipher.encrypt(payload)
def __str__(self):
@@ -152,39 +156,39 @@ def __str__(self):
def encode_payload(self):
'Return encoded, plaintext groups+entries buffer'
- payload = ""
+ payload = b""
for group in self.groups:
payload += group.encode()
for entry in self.entries:
payload += entry.encode()
return payload
+ def generate_contents_hash(self):
+ self.header.contents_hash = hashlib.sha256(self.encode_payload()).digest()
+
def write(self,filename,masterkey=""):
''''
Write out DB to given filename with optional master key.
If no master key is given, the one used to create this DB is used.
Resets IVs and master seeds.
'''
- import hashlib
-
+ self.generate_contents_hash()
+
header = copy(self.header)
header.ngroups = len(self.groups)
header.nentries = len(self.entries)
header.reset_random_fields()
-
- payload = self.encode_payload()
- header.contents_hash = hashlib.sha256(payload).digest()
-
+
finalkey = self.final_key(masterkey = masterkey or self.masterkey,
masterseed = header.master_seed,
masterseed2 = header.master_seed2,
rounds = header.key_enc_rounds)
- payload = self.encrypt_payload(payload, finalkey,
+ payload = self.encrypt_payload(self.encode_payload(), finalkey,
header.encryption_type(),
header.encryption_iv)
- fp = open(filename,'w')
+ fp = open(filename,'wb')
fp.write(header.encode())
fp.write(payload)
fp.close()
@@ -212,20 +216,19 @@ def dump_entries(self,format,show_passwords=False):
if 'group' not in nick: nick = 'group_'+nick
dat[nick] = group.__dict__[what]
- print format%dat
+ six.print_((format%dat))
continue
return
def hierarchy(self):
'''Return database with groups and entries organized into a
hierarchy'''
- from hier import Node
- top = Node()
+ top = hier.Node()
breadcrumb = [top]
node_by_id = {None:top}
for group in self.groups:
- n = Node(group)
+ n = hier.Node(group)
node_by_id[group.groupid] = n
while group.level - breadcrumb[-1].level() != 1:
@@ -235,7 +238,7 @@ def hierarchy(self):
breadcrumb[-1].nodes.append(n)
breadcrumb.append(n)
continue
-
+
for ent in self.entries:
n = node_by_id[ent.groupid]
n.entries.append(ent)
@@ -247,18 +250,18 @@ def update_by_hierarchy(self, hierarchy):
Update the database using the given hierarchy.
This replaces the existing groups and entries.
'''
- import hier
collector = hier.CollectVisitor()
hier.visit(hierarchy, collector)
self.groups = collector.groups
self.entries = collector.entries
+ self.generate_contents_hash()
return
def gen_groupid(self):
"""
Generate a new groupid (4-byte value that isn't 0 or 0xffffffff).
"""
- existing_groupids = {group.groupid for group in self.groups}
+ existing_groupids = set(group.groupid for group in self.groups)
if len(existing_groupids) >= 0xfffffffe:
raise Exception("All groupids are in use!")
while True:
@@ -266,7 +269,7 @@ def gen_groupid(self):
if groupid not in existing_groupids:
return groupid
- def update_entry(self,title,username,url,notes="",new_title=None,new_username=None,new_password=None,new_url=None,new_notes=None):
+ def update_entry(self,title,username,url,notes="",new_title="",new_username="",new_password="",new_url="",new_notes=""):
for entry in self.entries:
if entry.title == str(title) and entry.username == str(username) and entry.url == str(url):
if new_title: entry.title = new_title
@@ -274,73 +277,45 @@ def update_entry(self,title,username,url,notes="",new_title=None,new_username=No
if new_password: entry.password = new_password
if new_url: entry.url = new_url
if new_notes: entry.notes = new_notes
- entry.new_entry.last_mod_time = datetime.datetime.now()
+ entry.last_mod_time = datetime.datetime.now()
+ self.generate_contents_hash()
- def add_entry(self,path,title,username,password,url="",notes="",imageid=1,append=True):
+ def add_entry(self,path,title,username,password,url="",notes="",imageid=1):
'''
Add an entry to the current database at with given values. If
append is False a pre-existing entry that matches path, title
and username will be overwritten with the new one.
'''
- import hier, infoblock
top = self.hierarchy()
- node = hier.mkdir(top, path, self.gen_groupid)
-
- # fixme, this should probably be moved into a new constructor
- def make_entry():
- new_entry = infoblock.EntryInfo()
- new_entry.uuid = uuid.uuid4().hex
- new_entry.groupid = node.group.groupid
- new_entry.imageid = imageid
- new_entry.title = title
- new_entry.url = url
- new_entry.username = username
- new_entry.password = password
- new_entry.notes = notes
- new_entry.creation_time = datetime.datetime.now()
- new_entry.last_mod_time = datetime.datetime.now()
- new_entry.last_acc_time = datetime.datetime.now()
- new_entry.expiration_time = datetime.datetime(2999, 12, 28, 23, 59, 59) # KeePassX 0.4.3 default
- new_entry.binary_desc = ""
- new_entry.binary_data = None
- new_entry.order = [(1, 16),
- (2, 4),
- (3, 4),
- (4, len(title) + 1),
- (5, len(url) + 1),
- (6, len(username) + 1),
- (7, len(password) + 1),
- (8, len(notes) + 1),
- (9, 5),
- (10, 5),
- (11, 5),
- (12, 5),
- (13, len(new_entry.binary_desc) + 1),
- (14, 0),
- (65535, 0)]
- #new_entry.None = None
- #fixme, deal with times
- return new_entry
+ node = hier.mkdir(top, path, self.gen_groupid(), self.groups, self.header)
- existing_node_updated = False
- if not append:
- for i, ent in enumerate(node.entries):
- if ent.title != title: continue
- if ent.username != username: continue
- node.entries[i] = make_entry()
- existing_node_updated = True
- break
-
- if not existing_node_updated:
- node.entries.append(make_entry())
-
- self.update_by_hierarchy(top)
+ new_entry = EntryInfo().make_entry(node,title,username,password,url,notes,imageid)
+
+ self.entries.append(new_entry)
+ self.header.nentries += 1
+
+ self.generate_contents_hash()
+
+ def update_group(self, group_name="", groupid="", pathlen="", new_group_name="", new_groupid="", new_pathlen=""):
+ for group in self.groups:
+ if (group_name and group.group_name == group_name) and (groupid and group.groupid == groupid) and (pathlen and group.level == pathlen):
+ if new_group_name: group.group_name = new_group_name
+ if new_groupid: group.groupid = new_groupid
+ if new_pathlen: group.pathlen = new_pathlen
+ group.last_mod_time = datetime.datetime.now()
+ self.generate_contents_hash()
+
+ def add_group(self, path):
+ hier.mkdir(self.hierarchy(), path, self.gen_groupid(), self.groups, self.header)
+ self.generate_contents_hash()
def remove_entry(self, username, url):
for entry in self.entries:
if entry.username == str(username) and entry.url == str(url):
self.entries.remove(entry)
+ self.header.nentries -= 1
+ self.generate_contents_hash()
def remove_group(self, path, level=None):
for group in self.groups:
@@ -348,15 +323,19 @@ def remove_group(self, path, level=None):
if level:
if group.level == level:
self.groups.remove(group)
+ self.header.ngroups -= 1
for entry in self.entries:
if entry.groupid == group.groupid:
self.entries.remove(entry)
+ self.header.nentries -= 1
else:
self.groups.remove(group)
+ self.header.ngroups -= 1
for entry in self.entries:
if entry.groupid == group.groupid:
self.entries.remove(entry)
-
+ self.header.nentries -= 1
+ self.generate_contents_hash()
pass
diff --git a/python/keepass/test_crud.py b/python/keepass/test_crud.py
new file mode 100755
index 0000000..659feab
--- /dev/null
+++ b/python/keepass/test_crud.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python2
+
+import kpdb
+import six
+
+test = kpdb.Database()
+six.print_((test))
+test.write("test.kdb", "123")
+
+test = kpdb.Database("test.kdb", "123")
+
+six.print_((test))
+
+test.add_entry("Internet","test","test","test","test","test")
+
+test.add_entry("Internet","test1","test1","test1","test1","test1")
+
+test.remove_entry("test1","test1")
+
+six.print_((test))
+
+test.write("test.kdb", "123")
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 8a30ae0..afba732 100644
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,20 @@
from setuptools import setup
+from os.path import join, dirname
+import sys
+sys.path.append("python")
+
+import keepass as module
setup(
- name ='python-keepass',
- version='1.0',
- description='Command line and Python interfaces for operating on files in KeePass .kdb format',
+ name ='keepass',
+ version = module.__version__,
+ author = module.__author__,
+ author_email = module.__email__,
+ description = module.__description__,
+ license = module.__license__,
+ keywords = module.__keywords__,
+ url = module.__url__, # project home page, if any
+ long_description=open(join(dirname(__file__), 'README.txt')).read(),
package_dir={'': 'python'},
packages=['keepass'],
scripts=[
@@ -11,6 +22,14 @@
],
install_requires=[
'pycrypto',
+ 'six',
+ ],
+ classifiers=[
+ 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 3',
],
tests_require=[
'nose',
diff --git a/tests/test_de.py b/tests/test_de.py
index b2cad32..5ed403e 100644
--- a/tests/test_de.py
+++ b/tests/test_de.py
@@ -15,7 +15,7 @@ def test_shunt():
assert enc('foo') is 'foo'
def test_string():
- strings = ['foo','to encrypt or to decrypt, that is the ?','new\nline']
+ strings = [b'foo',b'to encrypt or to decrypt, that is the ?',b'new\nline']
dec,enc = ib.string_de()
for string in strings:
assert string == dec(enc(string))
diff --git a/tests/test_hier.py b/tests/test_hier.py
index 1873ac8..18747e1 100644
--- a/tests/test_hier.py
+++ b/tests/test_hier.py
@@ -1,5 +1,11 @@
from keepass import hier
+groups = []
+
+class Header():
+ def __init__(self):
+ self.ngroups = 0
+
class GroupIDGenerator(object):
def __init__(self):
self.groupid = 0
@@ -10,7 +16,7 @@ def gen_groupid(self):
def test_hierarchy():
top = hier.Node()
- hier.mkdir(top, 'SubDir/SubSubDir', GroupIDGenerator().gen_groupid)
+ hier.mkdir(top, 'SubDir/SubSubDir', GroupIDGenerator().gen_groupid, groups, Header())
dumper = hier.NodeDumper()
hier.walk(top,dumper)
diff --git a/tests/test_io.py b/tests/test_io.py
index 7e4f9a7..d45cada 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -14,15 +14,15 @@ def test_write():
try:
db = keepass.kpdb.Database()
db.add_entry(path='Secrets/Terrible', title='Gonk', username='foo', password='bar', url='https://example.org/')
- assert len(db.groups) == 2
- assert len(db.entries) == 1
+ assert len(db.groups) == 4
+ assert len(db.entries) == 3
db.write(kdb_path, password)
assert os.path.isfile(kdb_path)
db2 = keepass.kpdb.Database(kdb_path, password)
- assert len(db.groups) == 2
- assert len(db.entries) == 1
- assert db.entries[0].name() == 'Gonk'
+ assert len(db.groups) == 4
+ assert len(db.entries) == 3
+ assert db.entries[2].name() == 'Gonk'
finally:
shutil.rmtree(tempdir)