├── MANIFEST.in ├── test_safes ├── .gitignore ├── simple.psafe3 ├── VersionTest.psafe3 ├── EmptyGroupTest.psafe3 ├── LastSaveUserTest.psafe3 ├── RecentEntriesTest.psafe3 ├── passwordPolicyTest.psafe3 ├── NonDefaultPrefsTest.psafe3 └── unknown-record-prop-1.psafe3 ├── src └── pypwsafe │ ├── .gitignore │ ├── errors.py │ ├── consts.py │ ├── __init__.py │ └── PWSafeV3Headers.py ├── .gitignore ├── epydoc.cfg ├── LICENSE ├── tests ├── PWSv3Headers │ ├── __init__.py │ ├── RecentEntriesTest.py │ ├── EmptyGroupsTest.py │ ├── VersionTest.py │ ├── NonDefaultPrefsTest.py │ ├── LastSaveUser.py │ └── PasswdPolicyTest.py ├── runMe.py └── TestSafeTests.py ├── Headers.mediawiki ├── pypwsafe.spec ├── setup.py ├── README.md └── pwsafecli ├── test_pwsafecli.py ├── psafedump └── pwsafecli.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README 2 | -------------------------------------------------------------------------------- /test_safes/.gitignore: -------------------------------------------------------------------------------- 1 | /*.plk 2 | /*.ibak 3 | -------------------------------------------------------------------------------- /test_safes/simple.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/simple.psafe3 -------------------------------------------------------------------------------- /test_safes/VersionTest.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/VersionTest.psafe3 -------------------------------------------------------------------------------- /test_safes/EmptyGroupTest.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/EmptyGroupTest.psafe3 -------------------------------------------------------------------------------- /test_safes/LastSaveUserTest.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/LastSaveUserTest.psafe3 -------------------------------------------------------------------------------- /src/pypwsafe/.gitignore: -------------------------------------------------------------------------------- 1 | /__init__.pyc 2 | /consts.pyc 3 | /PWSafeV3Headers.pyc 4 | /errors.pyc 5 | /PWSafeV3Records.pyc 6 | -------------------------------------------------------------------------------- /test_safes/RecentEntriesTest.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/RecentEntriesTest.psafe3 -------------------------------------------------------------------------------- /test_safes/passwordPolicyTest.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/passwordPolicyTest.psafe3 -------------------------------------------------------------------------------- /test_safes/NonDefaultPrefsTest.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/NonDefaultPrefsTest.psafe3 -------------------------------------------------------------------------------- /test_safes/unknown-record-prop-1.psafe3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ronys/pypwsafe/HEAD/test_safes/unknown-record-prop-1.psafe3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.pydevproject 3 | /.settings 4 | /MANIFEST 5 | /dist 6 | /python-pypwsafe-0.1 7 | /Test Script.py 8 | /_gsdata_ 9 | /.externalToolBuilders 10 | /build 11 | /src/*.egg-info -------------------------------------------------------------------------------- /epydoc.cfg: -------------------------------------------------------------------------------- 1 | [epydoc] 2 | 3 | name: PyPWSafe 4 | url: https://github.com/ronys/pypwsafe 5 | 6 | modules: src/pypwsafe, src/pypwsafe/consts.py, src/pypwsafe/errors.py, src/pypwsafe/PWSafeV3Headers.py, src/pypwsafe/PWSafeV3Records.py 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | #=============================================================================== 2 | # SYMANTEC: Copyright © 2009-2011 Symantec Corporation. All rights reserved. 3 | # 4 | # This file is part of PyPWSafe. 5 | # 6 | # PyPWSafe is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # PyPWSafe is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 18 | #=============================================================================== -------------------------------------------------------------------------------- /tests/PWSv3Headers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | from PasswdPolicyTest import * 26 | from RecentEntriesTest import * 27 | from LastSaveUser import * 28 | from NonDefaultPrefsTest import * 29 | from EmptyGroupsTest import * 30 | from VersionTest import * 31 | -------------------------------------------------------------------------------- /tests/runMe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Run unit tests 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | import unittest 26 | import os, os.path, sys 27 | 28 | import logging 29 | logging.basicConfig( 30 | level = logging.DEBUG, 31 | filename = '/tmp/pypwsafe_unittests.log', 32 | filemode = 'w', 33 | ) 34 | 35 | from PWSv3Headers import * 36 | 37 | if __name__ == '__main__': 38 | sys.path.append("../src") 39 | unittest.main() 40 | -------------------------------------------------------------------------------- /Headers.mediawiki: -------------------------------------------------------------------------------- 1 | {| class="wikitable" 2 | +Database Headers 3 | !Name 4 | !Value 5 | !Type 6 | !PWS Version 7 | !PyPWSafe Support 8 | !Comments 9 | |- 10 | |Version 11 | |0x00 12 | |2 Bytes 13 | |Y 14 | |Y 15 | |Schema version 16 | |- 17 | |UUID 18 | |0x01 19 | |UUID 20 | |Y 21 | |Y 22 | | 23 | |- 24 | |Non-default preferences 25 | |0x02 26 | |Text 27 | |Y 28 | |Y 29 | | 30 | |- 31 | |Tree Display Status 32 | |0x03 33 | |Text 34 | |Y 35 | |Y 36 | | 37 | |- 38 | |Timestamp of last save 39 | |0x04 40 | |time_t 41 | |Y 42 | |Y 43 | | 44 | |- 45 | |Who performed last save 46 | |0x05 47 | |Text 48 | |Deprecated 49 | |Y 50 | |Deprecated as of 0x0302. PyPWSafe will read this tag and has write support for it. It will automatically convert it to the current header type. 51 | |- 52 | |What performed last save 53 | |0x06 54 | |Text 55 | |Y 56 | |Y 57 | | 58 | |- 59 | |Last saved by user 60 | |0x07 61 | |Text 62 | |Y 63 | |Y 64 | | 65 | |- 66 | |Last saved on host 67 | |0x08 68 | |Text 69 | |Y 70 | |Y 71 | | 72 | |- 73 | |Database Name 74 | |0x09 75 | |Text 76 | |Y 77 | |Y 78 | | 79 | |- 80 | |Database Description 81 | |0x0a 82 | |Text 83 | |Y 84 | |Y 85 | | 86 | |- 87 | |Database Filters 88 | |0x0b 89 | |Text 90 | |Y 91 | |Y 92 | | 93 | |- 94 | |Reserved 95 | |0x0c 96 | |n/a 97 | |Y 98 | |Y 99 | | 100 | |- 101 | |Reserved 102 | |0x0d 103 | |n/a 104 | |Y 105 | |Y 106 | | 107 | |- 108 | |Reserved 109 | |0x0e 110 | |n/a 111 | |Y 112 | |Y 113 | | 114 | |- 115 | |Recently Used Entries 116 | |0x0f 117 | |Text 118 | |Y 119 | |Y 120 | | 121 | |- 122 | |Named Password Policies 123 | |0x10 124 | |Text 125 | |Y 126 | |Y 127 | | 128 | |- 129 | |Empty Groups 130 | |0x11 131 | |Text 132 | |Y 133 | |Y 134 | | 135 | |- 136 | |Reserved 137 | |0x12 138 | |Text 139 | |Y 140 | |Y 141 | | 142 | |- 143 | |End of Entry 144 | |0xff 145 | |[Empty] 146 | |Y 147 | |Y 148 | |Must be last 149 | |} 150 | 151 | -------------------------------------------------------------------------------- /pypwsafe.spec: -------------------------------------------------------------------------------- 1 | Name: pypwsafe 2 | Summary: A Python library program for reading Password Safe files. 3 | 4 | Group: Applications/Internet 5 | License: GPLv2 6 | URL: https://github.com/ronys/pypwsafe 7 | Source0: %{name}-%{version}-%{release}.tgz 8 | BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) 9 | Requires: %{name}-lib = %{version}-%{release} 10 | 11 | %package lib 12 | Summary: A Python library for reading and writing Password Safe files. 13 | Group: Development/Libraries 14 | Requires: python-mcrypt 15 | Requires: python >= 2.4 16 | 17 | %package webui 18 | Summary: A Django-based web UI and RPC layer for interacting with one or more Password Safe files. 19 | Group: Applications/Internet 20 | Requires: Django >= 1.3 21 | Requires: python-mcrypt 22 | Requires: python >= 2.4 23 | # Not avail as RPMs yet 24 | #Requires: django-dajax 25 | #Requires: django-dajaxice 26 | #Requires: django-rpc4django 27 | 28 | %description 29 | FIXME 30 | 31 | %description webui 32 | FIXME 33 | 34 | %description lib 35 | FIXME 36 | 37 | %prep 38 | %setup -q 39 | 40 | 41 | %build 42 | 43 | 44 | %install 45 | rm -rf %{buildroot} 46 | /usr/bin/python setup.py install --root=%{buildroot} 47 | 48 | 49 | %clean 50 | rm -rf %{buildroot} 51 | 52 | 53 | %files 54 | %defattr(-,root,root,-) 55 | %doc 56 | /usr/bin/psafedump 57 | 58 | %files lib 59 | %dir /usr/lib/python2.6/site-packages/pypwsafe/ 60 | /usr/lib/python2.6/site-packages/pypwsafe/__init__.py* 61 | /usr/lib/python2.6/site-packages/pypwsafe/consts.py* 62 | /usr/lib/python2.6/site-packages/pypwsafe/errors.py* 63 | /usr/lib/python2.6/site-packages/pypwsafe/PWSafeV3Headers.py* 64 | /usr/lib/python2.6/site-packages/pypwsafe/PWSafeV3Records.py* 65 | %exclude /usr/lib/python2.6/site-packages/python_pypwsafe-*-py2.6.egg-info 66 | 67 | %files webui 68 | /usr/lib/python2.6/site-packages/psafefe/*.py* 69 | /usr/lib/python2.6/site-packages/psafefe/pws/*.py* 70 | /usr/lib/python2.6/site-packages/psafefe/pws/rpc/*.py* 71 | /usr/lib/python2.6/site-packages/psafefe/pws/tasks/*.py* 72 | 73 | /usr/share/psafefe/media/README 74 | /usr/share/psafefe/static/base.css 75 | %exclude /usr/share/psafefe/static/.gitignore 76 | /usr/share/psafefe/templates/*html 77 | /usr/share/psafefe/templates/registration/*html 78 | 79 | %changelog 80 | * Wed Jul 18 2012 Paulson McIntyre - 0.0-0 81 | - Initial version 82 | -------------------------------------------------------------------------------- /tests/PWSv3Headers/RecentEntriesTest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Test named and unnamed password policies 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | import unittest 26 | import os, os.path, sys 27 | 28 | from TestSafeTests import TestSafeTestBase, STANDARD_TEST_SAFE_PASSWORD 29 | 30 | 31 | class RecentEntriesTest_DBLevel(TestSafeTestBase): 32 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 33 | # All test safes must have the standard password (see above) 34 | testSafe = 'RecentEntriesTest.psafe3' 35 | # Automatically open safes 36 | autoOpenSafe = False 37 | # How to open the safe 38 | autoOpenMode = "RO" 39 | 40 | def _openSafe(self): 41 | from pypwsafe import PWSafe3 42 | self.testSafeO = PWSafe3( 43 | filename = self.ourTestSafe, 44 | password = STANDARD_TEST_SAFE_PASSWORD, 45 | mode = self.autoOpenMode, 46 | ) 47 | 48 | def test_open(self): 49 | self.testSafeO = None 50 | self._openSafe() 51 | self.assertTrue(self.testSafeO, "Failed to open the test safe") 52 | 53 | 54 | class RecentEntriesTest_RecordLevel(TestSafeTestBase): 55 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 56 | # All test safes must have the standard password (see above) 57 | testSafe = 'RecentEntriesTest.psafe3' 58 | # Automatically open safes 59 | autoOpenSafe = True 60 | # How to open the safe 61 | autoOpenMode = "RO" 62 | 63 | def test_entries(self): 64 | from uuid import UUID 65 | for entry in self.testSafeO.getDbRecentEntries(): 66 | self.assertTrue(type(entry) == UUID, "Expected a UUID") 67 | 68 | 69 | 70 | # FIXME: Add save test 71 | 72 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Install pypwsafe 19 | Created on Jul 23, 2011 20 | 21 | @author: paulson mcintyre 22 | ''' 23 | from setuptools import setup 24 | import sys 25 | VERSION = "0.3" 26 | 27 | # Generate docs 28 | import os 29 | sys.path.append('src') 30 | sys.path.append('tests') 31 | sys.path.append('pwsafecli') 32 | 33 | setup( 34 | name = "python-pypwsafe", 35 | version = VERSION, 36 | description = "Python interface to Password Safe v3 files", 37 | author = "Paulson McIntyre", 38 | author_email = "paul@gpmidi.net", 39 | license = "GPL", 40 | long_description = \ 41 | """ 42 | A Python interface for reading and writing Password Safe v3 43 | files. Includes support for Password Safe versions V3.01 44 | through V3.29Y. 45 | """, 46 | url = 'https://github.com/ronys/pypwsafe', 47 | packages = [ 48 | 'pypwsafe', 49 | ], 50 | package_dir = { 51 | '':'src', 52 | }, 53 | scripts = [ 54 | "pwsafecli/pwsafecli.py", 55 | "pwsafecli/psafedump", 56 | ], 57 | data_files = [], 58 | classifiers = [ 59 | "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", 60 | "Programming Language :: Python :: 2.6", 61 | "Programming Language :: Python :: 2.7", 62 | "Development Status :: 4 - Beta", 63 | "Operating System :: MacOS", 64 | "Operating System :: POSIX", 65 | "Intended Audience :: System Administrators", 66 | "Intended Audience :: Developers", 67 | "Topic :: Security :: Cryptography", 68 | "Topic :: Utilities", 69 | "Topic :: System :: Systems Administration", 70 | ], 71 | keywords = 'password login authentication passwordsafe security psafe3', 72 | install_requires = [ 73 | 'distribute', 74 | 'python2-mcrypt', 75 | 'hashlib', 76 | 'pycrypto', 77 | ], 78 | ) 79 | -------------------------------------------------------------------------------- /tests/TestSafeTests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | """ Test the pypwsafe API - Provides tests based on the different test safes. 19 | 20 | @author: Paulson McIntyre (GpMidi) 21 | @license: GPLv2 22 | @version: 0.1 23 | """ 24 | import unittest 25 | import os, os.path, sys 26 | from tempfile import mkdtemp 27 | from shutil import rmtree, copyfile 28 | # Password to decrypt all test safes 29 | STANDARD_TEST_SAFE_PASSWORD = 'bogus12345' 30 | 31 | class TestSafeTestBase(unittest.TestCase): 32 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 33 | # All test safes must have the standard password (see above) 34 | testSafe = None 35 | # Automatically open safes 36 | autoOpenSafe = True 37 | # How to open the safe 38 | autoOpenMode = "RO" 39 | 40 | def setUp(self): 41 | assert self.testSafe 42 | 43 | self.safeLoc = os.path.join("../test_safes", self.testSafe) 44 | assert os.access(self.safeLoc, os.R_OK) 45 | 46 | # Make a temp dir and make a copy 47 | self.safeDir = mkdtemp(prefix = "safe_test_%s" % type(self).__name__) 48 | 49 | # COpy the safe 50 | self.ourTestSafe = os.path.join( 51 | self.safeDir, 52 | os.path.basename(self.testSafe), 53 | ) 54 | copyfile(self.safeLoc, self.ourTestSafe) 55 | 56 | from pypwsafe import PWSafe3 57 | if self.autoOpenSafe: 58 | self.testSafeO = PWSafe3( 59 | filename = self.ourTestSafe, 60 | password = STANDARD_TEST_SAFE_PASSWORD, 61 | mode = self.autoOpenMode, 62 | ) 63 | else: 64 | self.testSafeO = None 65 | 66 | def tearDown(self): 67 | try: 68 | rmtree(self.safeDir) 69 | except: 70 | pass 71 | -------------------------------------------------------------------------------- /tests/PWSv3Headers/EmptyGroupsTest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Test empty group fields 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | import unittest 26 | import os, os.path, sys 27 | 28 | from TestSafeTests import TestSafeTestBase, STANDARD_TEST_SAFE_PASSWORD 29 | 30 | 31 | class EmptyGroupTest_DBLevel(TestSafeTestBase): 32 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 33 | # All test safes must have the standard password (see above) 34 | testSafe = 'EmptyGroupTest.psafe3' 35 | # Automatically open safes 36 | autoOpenSafe = False 37 | # How to open the safe 38 | autoOpenMode = "RO" 39 | 40 | def _openSafe(self): 41 | from pypwsafe import PWSafe3 42 | self.testSafeO = PWSafe3( 43 | filename = self.ourTestSafe, 44 | password = STANDARD_TEST_SAFE_PASSWORD, 45 | mode = self.autoOpenMode, 46 | ) 47 | 48 | def test_open(self): 49 | self.testSafeO = None 50 | self._openSafe() 51 | self.assertTrue(self.testSafeO, "Failed to open the test safe") 52 | 53 | 54 | class EmptyGroupTest_RecordLevel(TestSafeTestBase): 55 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 56 | # All test safes must have the standard password (see above) 57 | testSafe = 'EmptyGroupTest.psafe3' 58 | # Automatically open safes 59 | autoOpenSafe = True 60 | # How to open the safe 61 | autoOpenMode = "RO" 62 | 63 | def test_hasEmptyGroups(self): 64 | self.assertTrue('asdf' in self.testSafeO.getEmptyGroups(), "Expected an empty group named 'asdf'") 65 | self.assertTrue('fdas' in self.testSafeO.getEmptyGroups(), "Expected an empty group named 'fdas'") 66 | 67 | def test_addEmptyGroup(self): 68 | newgrp = 'bogus5324' 69 | self.testSafeO.addEmptyGroup(newgrp, updateAutoData = False) 70 | self.assertTrue(newgrp in self.testSafeO.getEmptyGroups(), "Expected an empty group named %r" % newgrp) 71 | 72 | 73 | # FIXME: Add save test 74 | 75 | -------------------------------------------------------------------------------- /src/pypwsafe/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # SYMANTEC: Copyright (C) 2009-2011 Symantec Corporation. All rights reserved. 4 | # 5 | # This file is part of PyPWSafe. 6 | # 7 | # PyPWSafe is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # PyPWSafe is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 19 | #=============================================================================== 20 | """ Various errors the library can generate 21 | 22 | @author: Paulson McIntyre 23 | @license: GPLv2 24 | @version: 0.1 25 | """ 26 | class PSafeError(StandardError): 27 | """Base passsafe error""" 28 | 29 | class PasswordError(PSafeError): 30 | """Password does not match the password safe""" 31 | 32 | class InvalidHMACError(PSafeError): 33 | """Calculated HMAC does not equal HMAC in the file""" 34 | 35 | class ROSafe(PSafeError): 36 | """ Safe is not in read/write mode """ 37 | 38 | class UUIDNotFoundError(PSafeError): 39 | """UUID was not found""" 40 | 41 | class RecordError(PSafeError): 42 | """Failed to perform an action in a record""" 43 | 44 | class AccessError(PSafeError): 45 | """Insufficient permissions to access a psafe file""" 46 | 47 | class ROSafeError(PSafeError): 48 | """A write request was made on a read-only safe""" 49 | 50 | class PropError(RecordError): 51 | """Failed to perform an action with a property""" 52 | 53 | class PropParsingError(PropError): 54 | """Failed to parse a property""" 55 | 56 | class HeaderError(PSafeError): 57 | """ Error in the headers """ 58 | 59 | class PrefrencesHeaderError(HeaderError): 60 | """ An error occurred in the preferences header """ 61 | 62 | class PrefsValueError(PrefrencesHeaderError): 63 | """ Unexpected or improper value for the header preference """ 64 | 65 | class PrefsDataTypeError(PrefrencesHeaderError): 66 | """ Error parsing the preferences type of the preferences 67 | header record""" 68 | 69 | class ConfigItemNotFoundError(PrefrencesHeaderError): 70 | """ No such preference """ 71 | 72 | class UnableToFindADelimitersError(PrefrencesHeaderError): 73 | """ Couldn't find an unused char to delminate the string""" 74 | 75 | class AlreadyLockedError(RuntimeError): 76 | """ The psafe in question is already locked. Can't acquire a new lock. """ 77 | 78 | class LockAlreadyAcquiredError(AlreadyLockedError): 79 | """ The psafe in question is already locked by this psafe object. Can't acquire a new lock. """ 80 | 81 | class NotLockedError(RuntimeError): 82 | """ The psafe in question is not locked. Can't unlock. """ 83 | 84 | -------------------------------------------------------------------------------- /tests/PWSv3Headers/VersionTest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Test the version fields 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | import unittest 26 | import os, os.path, sys 27 | 28 | from TestSafeTests import TestSafeTestBase, STANDARD_TEST_SAFE_PASSWORD 29 | 30 | 31 | class VersionTest_DBLevel(TestSafeTestBase): 32 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 33 | # All test safes must have the standard password (see above) 34 | testSafe = 'VersionTest.psafe3' 35 | # Automatically open safes 36 | autoOpenSafe = False 37 | # How to open the safe 38 | autoOpenMode = "RO" 39 | 40 | def _openSafe(self): 41 | from pypwsafe import PWSafe3 42 | self.testSafeO = PWSafe3( 43 | filename = self.ourTestSafe, 44 | password = STANDARD_TEST_SAFE_PASSWORD, 45 | mode = self.autoOpenMode, 46 | ) 47 | 48 | def test_open(self): 49 | self.testSafeO = None 50 | self._openSafe() 51 | self.assertTrue(self.testSafeO, "Failed to open the test safe") 52 | 53 | 54 | class VersionTest_RecordLevel(TestSafeTestBase): 55 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 56 | # All test safes must have the standard password (see above) 57 | testSafe = 'VersionTest.psafe3' 58 | # Automatically open safes 59 | autoOpenSafe = True 60 | # How to open the safe 61 | autoOpenMode = "RW" 62 | 63 | def test_read(self): 64 | self.assertTrue(self.testSafeO.getVersion() is None, "Given safe shouldn't have a version") 65 | # self.assertTrue(self.testSafeO.getVersionPretty(), "Couldn't get the pretty version") 66 | 67 | def test_pretty_write(self): 68 | self.testSafeO.setVersionPretty(version = "PasswordSafe V3.28") 69 | self.testSafeO.save() 70 | self.assertTrue(self.testSafeO.getVersion() == 0x030A, "Pretty version set resulted in the wrong version ID") 71 | 72 | def test_bad_pretty_value(self): 73 | self.assertRaises(ValueError, self.testSafeO.setVersionPretty, version = "Bogus version") 74 | 75 | 76 | # FIXME: Add save test 77 | 78 | -------------------------------------------------------------------------------- /tests/PWSv3Headers/NonDefaultPrefsTest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Test non-default preferences for the DB 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | import unittest 26 | import os, os.path, sys 27 | 28 | from TestSafeTests import TestSafeTestBase, STANDARD_TEST_SAFE_PASSWORD 29 | 30 | 31 | class NonDefaultPrefsTest_DBLevel(TestSafeTestBase): 32 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 33 | # All test safes must have the standard password (see above) 34 | testSafe = 'NonDefaultPrefsTest.psafe3' 35 | # Automatically open safes 36 | autoOpenSafe = False 37 | # How to open the safe 38 | autoOpenMode = "RO" 39 | 40 | def _openSafe(self): 41 | from pypwsafe import PWSafe3 42 | self.testSafeO = PWSafe3( 43 | filename = self.ourTestSafe, 44 | password = STANDARD_TEST_SAFE_PASSWORD, 45 | mode = self.autoOpenMode, 46 | ) 47 | 48 | def test_open(self): 49 | self.testSafeO = None 50 | self._openSafe() 51 | self.assertTrue(self.testSafeO, "Failed to open the test safe") 52 | 53 | 54 | class NonDefaultPrefsTest_RecordLevel(TestSafeTestBase): 55 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 56 | # All test safes must have the standard password (see above) 57 | testSafe = 'NonDefaultPrefsTest.psafe3' 58 | # Automatically open safes 59 | autoOpenSafe = True 60 | # How to open the safe 61 | autoOpenMode = "RO" 62 | 63 | def test_defaults(self): 64 | from pypwsafe.consts import conf_bools, conf_ints, conf_strs, ptDatabase 65 | prefs = self.testSafeO.getDbPrefs() 66 | self.assertTrue(len(prefs) > 0, "Expected some prefs to be set") 67 | 68 | # print repr(prefs) 69 | 70 | for typeS in [conf_bools, conf_ints, conf_strs]: 71 | for name, info in typeS.items(): 72 | if info['type'] == ptDatabase: 73 | self.assertTrue(name in prefs, "Didn't find %r in %r" % (name, prefs)) 74 | else: 75 | self.assertFalse(name in prefs, "Found %r of type %r in %r when it's not a DB level setting" % (name, info['type'], prefs)) 76 | 77 | # FIXME: Add a check to make sure default values aren't being saved 78 | # FIXME: Add save test 79 | 80 | -------------------------------------------------------------------------------- /tests/PWSv3Headers/LastSaveUser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Test the last saving user fields 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | import unittest 26 | import os, os.path, sys 27 | 28 | from TestSafeTests import TestSafeTestBase, STANDARD_TEST_SAFE_PASSWORD 29 | 30 | 31 | class LastSaveUserTest_DBLevel(TestSafeTestBase): 32 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 33 | # All test safes must have the standard password (see above) 34 | testSafe = 'LastSaveUserTest.psafe3' 35 | # Automatically open safes 36 | autoOpenSafe = False 37 | # How to open the safe 38 | autoOpenMode = "RO" 39 | 40 | def _openSafe(self): 41 | from pypwsafe import PWSafe3 42 | self.testSafeO = PWSafe3( 43 | filename = self.ourTestSafe, 44 | password = STANDARD_TEST_SAFE_PASSWORD, 45 | mode = self.autoOpenMode, 46 | ) 47 | 48 | def test_open(self): 49 | self.testSafeO = None 50 | self._openSafe() 51 | self.assertTrue(self.testSafeO, "Failed to open the test safe") 52 | 53 | 54 | class LastSaveUserTest_RecordLevel(TestSafeTestBase): 55 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 56 | # All test safes must have the standard password (see above) 57 | testSafe = 'LastSaveUserTest.psafe3' 58 | # Automatically open safes 59 | autoOpenSafe = True 60 | # How to open the safe 61 | autoOpenMode = "RW" 62 | 63 | def test_write(self): 64 | found = self.testSafeO.getLastSaveUserNew() 65 | self.assertTrue(found, "Didn't find a new username") 66 | 67 | username = 'user123' 68 | self.testSafeO.setLastSaveUser( 69 | username = username, 70 | updateAutoData = True, 71 | addOld = True, 72 | ) 73 | found = self.testSafeO.getLastSaveUserNew() 74 | foundOld = self.testSafeO.getLastSaveUserOld() 75 | self.assertTrue(found == username, "Saved new user doesn't match what we set") 76 | self.assertTrue(foundOld == username, "Saved old user (%r) doesn't match what we set (%r)" % (foundOld, username)) 77 | 78 | def test_new(self): 79 | found = self.testSafeO.getLastSaveUserNew() 80 | 81 | def test_old(self): 82 | found = self.testSafeO.getLastSaveUserOld() 83 | self.assertFalse(found, "Found an old username") 84 | 85 | def test_base(self): 86 | foundN = self.testSafeO.getLastSaveUserNew() 87 | foundO = self.testSafeO.getLastSaveUserOld() 88 | 89 | foundFB = self.testSafeO.getLastSaveUser(fallbackOld = True) 90 | 91 | self.assertTrue( 92 | foundN == foundFB or foundO == foundFB, 93 | "User mismatch", 94 | ) 95 | 96 | # FIXME: Add save test 97 | 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | A pure-Python2 library that can read and write Password Safe v3 4 | files. It includes full support for almost all current Password 5 | Safe v3 database headers and record headers. 6 | 7 | Note 8 | ==== 9 | Since this was written, the main (PasswordSafe project)[https://github.com/pwsafe/pwsafe] has added a cli version (pwsafe-cli) as part of the deliverables. That may be preferable, both for performance and compatibility, to using this. 10 | 11 | History 12 | ======= 13 | The library was initially written by Paulson McIntyre for 14 | Symantec in 2009. It was later released by Symantec under the 15 | GPLv2 in 2011. Changes and updates have been made since by Paulson 16 | McIntyre (GpMidi), Evan Deaubl (evandeaubl), and Sean Perry (shaleh). 17 | Rony Shapiro maintains the project page and acts as gate keeper 18 | for new patches. 19 | 20 | Known Issues 21 | ============ 22 | 1. Lack of documentation 23 | 2. Unit tests are out-of-date 24 | 3. There MAY be an issue with the order that NonDefaultPrefsHeader serializes preferences for HMAC validation in pypwsafe. Although the library validates HMACs fine at the moment, so who knows. 25 | 4. The version of python-mcrypt for Windows isn't compatible with this library. As a result, the pypwsafe library doesn't work in Windows. If anyone is able to get around this, please notify us. The library has not been tried under Cygwin. 26 | 27 | Dependencies 28 | ============ 29 | 1. python2-mcrypt 30 | 2. hashlib OR pycrypto 31 | 32 | Install Instructions 33 | ==================== 34 | 35 | Docker 36 | ------ 37 | Here's a Dockerfile from liath 38 | ``` 39 | FROM python:2-alpine 40 | 41 | RUN apk add --no-cache gcc git libmcrypt-dev musl-dev && \ 42 | pip install python2-mcrypt pycrypto && \ 43 | git clone https://github.com/ronys/pypwsafe.git /app 44 | WORKDIR /app 45 | RUN python setup.py install && python -c "import pypwsafe" 46 | 47 | ENTRYPOINT ["/usr/local/bin/python", "/app/pwsafecli/psafedump"] 48 | ``` 49 | To run: 50 | ``` 51 | docker build -t pypwsafe . 52 | docker run --rm -it -v "${PWD}:/work" --workdir /work pypwsafe -f /work/Backup.psafe3 -p xxx 53 | ``` 54 | 55 | RHEL/CentOS 56 | ----------- 57 | 1. Install libmcrypt and it's dev package along with the Python dev package: 58 | yum install libmcrypt-devel libmcrypt python-devel 59 | These packages are needed by the installer for python2-mcrypt 60 | 2. Install the standard Linux development tools. For RHEL/CentOS 5 and 6, `yum groupinstall 'Development tools'` can be used if your YUM repos have group information. 61 | 3. Use Pip or easy install to install python2-mcrypt, hashlib, and pycrypto 62 | 4. Run the setup script 63 | python setup.py install 64 | 5. Test that the module loads 65 | python -c "import pypwsafe" 66 | 67 | MACOSX 68 | --------- 69 | 1. Install clang compiler using brew: 70 | brew install llvm 71 | 2. Install mcrypt using brew: 72 | brew install mcrypt 73 | This package is needed by the installer for python2-mcrypt 74 | 3. Use Pip or easy install to install python2-mcrypt, hashlib, and pycrypto 75 | 4. Run the setup script 76 | python setup.py install 77 | 5. Test that the module loads 78 | python -c "import pypwsafe" 79 | 80 | Windows 81 | ------- 82 | Windows is not currently supported due to issues with python-mcrypt. A 83 | pure-Python Twofish implementation will allow future support, if a bit 84 | slower than a C-based implementation. 85 | 86 | Development Setup Instructions 87 | ------------------------------ 88 | FIXME: Fill this in 89 | 90 | FAQ 91 | === 92 | ### Why mcrypt and not use PyCrypto? 93 | The pyCrypto library doesn't support TwoFish, which is a newer cipher based on Blowfish. Twofish is required to encrypt/decrypt Password Safe v3 files. 94 | 95 | ### Where can I find details on the Password Safe file format? 96 | The format spec is kept in the Password Safe project's SVN repo. Go 97 | to the password safe code base and check in /pwsafe/pwsafe/docs/formatV3.txt. 98 | As of today, it can be found [here](http://sourceforge.net/p/passwordsafe/code/5210/tree/trunk/pwsafe/pwsafe/docs/) 99 | 100 | TODO 101 | ==== 102 | 1. Add support for using a pure-python TwoFish algorithm if mcrypt doesn't work. 103 | http://code.google.com/p/python-keysafe/source/browse/crypto/twofish.py 104 | http://www.bjrn.se/code/twofishpy.txt 105 | 2. Need to update against the latest version of the official psafe format v3 doc. 106 | -------------------------------------------------------------------------------- /tests/PWSv3Headers/PasswdPolicyTest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' Test named and unnamed password policies 19 | Created on Jan 19, 2013 20 | 21 | @author: Paulson McIntyre (GpMidi) 22 | @license: GPLv2 23 | @version: 0.1 24 | ''' 25 | import unittest 26 | import os, os.path, sys 27 | 28 | from TestSafeTests import TestSafeTestBase, STANDARD_TEST_SAFE_PASSWORD 29 | 30 | 31 | class NamedPolicyTest_DBLevel(TestSafeTestBase): 32 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 33 | # All test safes must have the standard password (see above) 34 | testSafe = 'passwordPolicyTest.psafe3' 35 | # Automatically open safes 36 | autoOpenSafe = False 37 | # How to open the safe 38 | autoOpenMode = "RO" 39 | 40 | def _openSafe(self): 41 | from pypwsafe import PWSafe3 42 | self.testSafeO = PWSafe3( 43 | filename = self.ourTestSafe, 44 | password = STANDARD_TEST_SAFE_PASSWORD, 45 | mode = self.autoOpenMode, 46 | ) 47 | 48 | def test_open(self): 49 | self.testSafeO = None 50 | self._openSafe() 51 | self.assertTrue(self.testSafeO, "Failed to open the test safe") 52 | 53 | 54 | class NamedPolicyTest_RecordLevel(TestSafeTestBase): 55 | # Should be overridden with a test safe file name. The path should be relative to the test_safes directory. 56 | # All test safes must have the standard password (see above) 57 | testSafe = 'passwordPolicyTest.psafe3' 58 | # Automatically open safes 59 | autoOpenSafe = True 60 | # How to open the safe 61 | autoOpenMode = "RO" 62 | 63 | FIXED_POLICIES = { 64 | "Policy 1":{ 65 | 'useLowercase':True, 66 | 'useUppercase':True, 67 | 'useDigits':True, 68 | 'useSymbols':False, 69 | 'useHexDigits':False, 70 | 'useEasyVision':False, 71 | 'makePronounceable':False, 72 | 'minTotalLength':16, 73 | 'minLowercaseCharCount':3, 74 | 'minUppercaseCharCount':2, 75 | 'minDigitCount':1, 76 | 'minSpecialCharCount':0, 77 | 'allowedSpecialSymbols':"+-=_@#$%^&;:,.<>/~\\[](){}?!|", 78 | }, 79 | "Policy Hex":{ 80 | 'useLowercase':False, 81 | 'useUppercase':False, 82 | 'useDigits':False, 83 | 'useSymbols':False, 84 | 'useHexDigits':True, 85 | 'useEasyVision':False, 86 | 'makePronounceable':False, 87 | 'minTotalLength':20, 88 | 'minLowercaseCharCount':1, 89 | 'minUppercaseCharCount':1, 90 | 'minDigitCount':1, 91 | 'minSpecialCharCount':1, 92 | 'allowedSpecialSymbols':"+-=_@#$%^&;:,.<>/~\\[](){}?!|", 93 | }, 94 | "Policy Long":{ 95 | 'useLowercase':True, 96 | 'useUppercase':True, 97 | 'useDigits':True, 98 | 'useSymbols':True, 99 | 'useHexDigits':False, 100 | 'useEasyVision':True, 101 | 'makePronounceable':False, 102 | 'minTotalLength':30, 103 | 'minLowercaseCharCount':1, 104 | 'minUppercaseCharCount':1, 105 | 'minDigitCount':1, 106 | 'minSpecialCharCount':1, 107 | 'allowedSpecialSymbols':"+-=_@#$%^<>/~\\?", 108 | }, 109 | } 110 | 111 | def test_flags(self): 112 | for policy in self.testSafeO.getDbPolicies(): 113 | if policy['name'] in self.FIXED_POLICIES: 114 | for k, v in self.FIXED_POLICIES[policy['name']].items(): 115 | self.assertTrue(k in policy, "%r: Expected %r to be in %r" % (policy['name'], k, policy)) 116 | self.assertEqual(policy[k], v, "%r: Expected %r from %r to equal %r" % (policy['name'], policy[k], k, v)) 117 | 118 | # FIXME: Add save test 119 | 120 | -------------------------------------------------------------------------------- /pwsafecli/test_pwsafecli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import copy 4 | from mock import Mock 5 | from nose.tools import assert_equals 6 | import optparse 7 | import os 8 | import sys 9 | 10 | import pwsafecli 11 | 12 | # available in newer versions 13 | class AssertRaises(object): 14 | def __init__(self, exception): 15 | self.expected_exception = exception 16 | self.exception = None 17 | 18 | def __enter__(self): 19 | pass 20 | 21 | def __exit__(self, exctype, value, tb): 22 | assert_equals(exctype, self.expected_exception) 23 | 24 | self.exception = exctype(value) 25 | return True 26 | 27 | def test_get_record_attr(): 28 | record = Mock(spec=["getFoo", "getBar"]) 29 | record.getFoo = Mock(return_value="foo") 30 | record.getBar = Mock(return_value=False) 31 | 32 | assert_equals("foo", pwsafecli.get_record_attr(record, "foo")) 33 | assert_equals(False, pwsafecli.match_valid(record, **{})) 34 | assert_equals(True, pwsafecli.match_valid(record, **{"foo": "foo"})) 35 | assert_equals(True, pwsafecli.match_valid(record, **{"foo": "foo", "title": None})) 36 | assert_equals(False, pwsafecli.match_valid(record, **{"foo": "foo", "bar": True})) 37 | with AssertRaises(AttributeError): 38 | pwsafecli.match_valid(record, **{"waz": object()}) 39 | 40 | def try_collect_record_options_with_attrs(attrs): 41 | mock = Mock(spec=["group", "title", "username", "UUID"]) 42 | mock.group = None 43 | mock.title = None 44 | mock.username = None 45 | mock.UUID = None 46 | for key in attrs.keys(): 47 | setattr(mock, key, attrs[key]) 48 | 49 | result = pwsafecli.collect_record_options(mock) 50 | assert_equals(attrs, result) 51 | 52 | def test_collect_record_options(): 53 | try_collect_record_options_with_attrs({}) 54 | try_collect_record_options_with_attrs({"group": ['foo']}) 55 | try_collect_record_options_with_attrs({'title': 'Test Node'}) 56 | try_collect_record_options_with_attrs({'username': 'bob'}) 57 | try_collect_record_options_with_attrs({'UUID': '1-1-1-1'}) 58 | 59 | class TestCommandLine(object): 60 | def __init__(self): 61 | self.parsers = pwsafecli.makeArgParser() 62 | self.orig_stderr = copy.deepcopy(sys.stderr) 63 | sys.stderr = open(os.devnull, "wb") 64 | self.orig_stdout = copy.deepcopy(sys.stdout) 65 | sys.stdout = open(os.devnull, "wb") 66 | 67 | def __del__(self): 68 | sys.stdout = self.orig_stdout 69 | sys.stderr = self.orig_stderr 70 | 71 | def test_no_action(self): 72 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 73 | pwsafecli.parse_commandline(self.parsers, ['unittest',]) 74 | 75 | def test_no_action_help(self): 76 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 77 | pwsafecli.parse_commandline(self.parsers, "unittest --help".split()) 78 | 79 | def test_unknown(self): 80 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 81 | pwsafecli.parse_commandline(self.parsers, "unittest --what".split()) 82 | 83 | def test_unknown_command(self): 84 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 85 | pwsafecli.parse_commandline(self.parsers, 86 | "unittest unknown".split()) 87 | 88 | def test_add_no_options(self): 89 | with AssertRaises(SystemExit) as cm: 90 | options = pwsafecli.parse_commandline(self.parsers, 91 | "unittest add".split()) 92 | 93 | def test_add_filename_no_options(self): 94 | options = pwsafecli.parse_commandline(self.parsers, 95 | "unittest add --file foo".split()) 96 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 97 | pwsafecli.add_validator(options) 98 | 99 | def test_add_missing_password(self): 100 | options = pwsafecli.parse_commandline(self.parsers, 101 | "unittest add --file foo --title blah --username me --group foo.bar.baz".split()) 102 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 103 | pwsafecli.add_validator(options) 104 | 105 | def test_add_expires_option(self): 106 | options = pwsafecli.parse_commandline(self.parsers, 107 | ["unittest", "add", "--file", "foo", "--title", "blah", "--username", "me", "--group", "foo.bar.baz", "--password", "secret", "--expires", "2012-01-01 00:00"]) 108 | pwsafecli.add_validator(options) 109 | 110 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 111 | options = pwsafecli.parse_commandline(self.parsers, 112 | ["unittest", "add", "--file", "foo", "--title", "blah", "--username", "me", "--group", "foo.bar.baz", "--password", "secret", "--expires", "2012-01-01 00:00:00 MDT"]) 113 | 114 | def test_delete_missing_uuid(self): 115 | options = pwsafecli.parse_commandline(self.parsers, 116 | "unittest delete --file foo".split()) 117 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 118 | pwsafecli.delete_validator(options) 119 | 120 | def test_get_missing_options(self): 121 | for cmdline in ("unittest get --file foo", "unittest get --file foo --email foo@bar"): 122 | print cmdline 123 | options = pwsafecli.parse_commandline(self.parsers, 124 | cmdline.split()) 125 | with AssertRaises(pwsafecli.PWSafeCLIValidationError): 126 | pwsafecli.get_validator(options) 127 | 128 | def test_get_display_option(self): 129 | options = pwsafecli.parse_commandline(self.parsers, "unittest get --file foo --uuid f2dee5f8-e964-402f-9fe7-78bd2b5cba2e --display username,password".split()) 130 | pwsafecli.get_validator(options) 131 | 132 | options = pwsafecli.parse_commandline(self.parsers, "unittest get --file foo --uuid f2dee5f8-e964-402f-9fe7-78bd2b5cba2e --display username,password,uuid".split()) 133 | pwsafecli.get_validator(options) 134 | 135 | options = pwsafecli.parse_commandline(self.parsers, "unittest get --file foo --uuid f2dee5f8-e964-402f-9fe7-78bd2b5cba2e --display username,password,uuid,missing".split()) 136 | 137 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 138 | pwsafecli.get_validator(options) 139 | 140 | def test_init_options(self): 141 | options = pwsafecli.parse_commandline(self.parsers, "unittest init --file foo".split()) 142 | pwsafecli.init_validator(options) 143 | 144 | def test_update_missing_uuid(self): 145 | options = pwsafecli.parse_commandline(self.parsers, 146 | "unittest update --file foo --username test".split()) 147 | with AssertRaises(pwsafecli.PWSafeCLIValidationError) as cm: 148 | pwsafecli.update_validator(options) 149 | -------------------------------------------------------------------------------- /pwsafecli/psafedump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | ''' A CLI interface for PyPWSafe. 19 | 20 | Allow users to view Password Safe files. 21 | 22 | Created on Jul 22, 2011 23 | 24 | @author: paulson mcintyre 25 | @author: steve 26 | ''' 27 | import logging, logging.config 28 | logger = logging.getLogger("psafebin.dump") 29 | log = logger.log 30 | logging.basicConfig( 31 | level = logging.INFO, 32 | format = '%(asctime)s %(levelname)s %(message)s', 33 | ) 34 | log(10, 'initing') 35 | import os, sys, os.path 36 | # FIXME: Add in tests to make sure we can import all 37 | # required libraries and generate non-coder errors 38 | # as to what is missing. 39 | #try: 40 | import pypwsafe as pws 41 | #except ImportError: 42 | # log(50, "Can't find the pypwsafe library") 43 | # sys.exit(1) 44 | 45 | from optparse import OptionParser 46 | 47 | # TODO: Find a simpler way of doing this 48 | def show_entry(entry, opts): 49 | """ Return true if an entry should be displayed """ 50 | m = ( 51 | ('Group', opts.filter_group), 52 | ('UUID', opts.filter_uuid), 53 | ('Title', opts.filter_title), 54 | ('Username', opts.filter_username), 55 | ) 56 | def match(e_name, lst): 57 | """ Return true if the given entry properity, 58 | e_name, is in lst or lst is empty. 59 | """ 60 | if len(lst) > 0: 61 | if entry[e_name] in lst: 62 | return True 63 | else: 64 | return False 65 | else: 66 | return True 67 | 68 | for e_name, lst in m: 69 | if not match(e_name, lst): 70 | return False 71 | 72 | return True 73 | 74 | def display_xml(entries, opts): 75 | raise NotImplementedError 76 | 77 | from StringIO import StringIO 78 | from csv import DictWriter 79 | import datetime 80 | def display_csv(entries, opts): 81 | csvFields = [ 82 | 'Group', 83 | 'Title', 84 | 'Username', 85 | 'Password', 86 | 'UUID', 87 | 'Note', 88 | 'Created', 89 | 'Modified', 90 | 'Last Access', 91 | 'Expires', 92 | 'URL', 93 | 'AutoType', 94 | 'History', 95 | ] 96 | by_groups = {} 97 | for entry in entries: 98 | group = '.'.join(entry['Group']) 99 | if not by_groups.has_key(group): 100 | by_groups[group] = [] 101 | by_groups[group].append(entry) 102 | groups = by_groups.keys() 103 | groups.sort() 104 | csvString = StringIO() 105 | c = DictWriter(csvString, csvFields) 106 | c.writerow(dict(zip(csvFields, csvFields))) 107 | for group in groups: 108 | by_groups[group].sort(lambda a, b: cmp(a['Title'] + a['Username'], b['Title'] + b['Username'])) 109 | for entry in by_groups[group]: 110 | policy = entry['PasswordPolicy'] 111 | history = [] 112 | # May want to add in dt at some point 113 | for dt, pwd in entry['PasswordHistory']['history'].items(): 114 | history.append(pwd) 115 | c.writerow({ 116 | 'Group':'.'.join(entry['Group']), 117 | 'Title':entry['Title'], 118 | 'Username':entry['Username'], 119 | 'Password':entry['Password'], 120 | 'UUID':entry['UUID'], 121 | 'Note':entry['Notes'], 122 | 'Created':datetime.datetime(*entry['ctime'][:6]).isoformat(), 123 | 'Modified':datetime.datetime(*entry['mtime'][:6]).isoformat(), 124 | 'Last Access':datetime.datetime(*entry['LastAccess'][:6]).isoformat(), 125 | 'Expires':datetime.datetime(*entry['PasswordExpiry'][:6]).isoformat(), 126 | 'URL':entry['URL'], 127 | 'AutoType':entry['Autotype'], 128 | # TODO: Add in support for creating new rows for each history entry 129 | 'History':';'.join(history), 130 | }) 131 | return csvString.getvalue() 132 | 133 | def display_display(entries, opts): 134 | ret = '' 135 | # Group by group 136 | by_groups = {} 137 | for entry in entries: 138 | group = '.'.join(entry['Group']) 139 | if not by_groups.has_key(group): 140 | by_groups[group] = [] 141 | by_groups[group].append(entry) 142 | groups = by_groups.keys() 143 | # In place! 144 | groups.sort() 145 | for group in groups: 146 | ret += "= %r =\n" % group 147 | by_groups[group].sort(lambda a, b: cmp(a['Title'] + a['Username'], b['Title'] + b['Username'])) 148 | for entry in by_groups[group]: 149 | ret += """ == %(Title)r == 150 | Username: %(Username)r 151 | Password: %(Password)r 152 | UUID: %(UUID)r 153 | URL: %(URL)r 154 | AutoType: %(Autotype)r 155 | """ % entry 156 | return ret 157 | 158 | 159 | if __name__ == "__main__": 160 | # Setup option parser to parse out args 161 | parser = OptionParser( 162 | usage = "%prog", 163 | version = "%prog v0.1", 164 | prog = "psafedump", 165 | description = """ 166 | A CLI interface for viewing Password Safe files. 167 | """, 168 | ) 169 | # TODO: Support multiple files 170 | parser.add_option( 171 | "-f", 172 | "--file", 173 | action = "store", 174 | type = "string", 175 | dest = "filename", 176 | default = None, 177 | help = "Password Safe file to create, edit, or view. ", 178 | ) 179 | parser.add_option( 180 | "-r", 181 | "--readonly", 182 | action = "store_true", 183 | dest = "readonly", 184 | default = False, 185 | help = "Open the Password Safe in read-only mode. [Default: %default]", 186 | ) 187 | parser.add_option( 188 | "-p", 189 | "--password", 190 | action = "append", 191 | dest = "password", 192 | default = [], 193 | help = "The password to use when opening the file. If given multiple times each password given will be tried. If not given, a password will be prompted for via STDIN and STDOUT. Note: Specifying the password via an argument is INSECURE. ", 194 | ) 195 | parser.add_option( 196 | "-d", 197 | "--debug", 198 | action = "store_true", 199 | dest = "debug", 200 | default = False, 201 | help = "Run in debug mode. Running in this mode WILL EXPOSE YOUR PASSWORDS and other sensitive information. [Default: %default]", 202 | ) 203 | # Output format 204 | parser.add_option( 205 | "--xml", 206 | action = "store_true", 207 | default = False, 208 | dest = "xml", 209 | help = "Display results in XML", 210 | ) 211 | parser.add_option( 212 | "--csv", 213 | action = "store_true", 214 | default = False, 215 | dest = "csv", 216 | help = "Display results in CSV", 217 | ) 218 | parser.add_option( 219 | "--display", 220 | action = "store_true", 221 | default = False, 222 | dest = "display", 223 | help = "Display results in a human readable format", 224 | ) 225 | 226 | # Display Filters 227 | parser.add_option( 228 | "--uuid", 229 | action = "append", 230 | dest = "filter_uuid", 231 | default = [], 232 | help = "Limit display to entries with this UUID. If given multiple times then displayed entries must match at least one of the UUIDs. ", 233 | ) 234 | parser.add_option( 235 | "--title", 236 | action = "append", 237 | dest = "filter_title", 238 | default = [], 239 | help = "Limit display to entries with this title. If given multiple times then displayed entries must match at least one of the titles. ", 240 | ) 241 | parser.add_option( 242 | "--group", 243 | action = "append", 244 | dest = "filter_group", 245 | default = [], 246 | help = "Limit display to entries with this group. If given multiple times then displayed entries must match at least one of the groups. ", 247 | ) 248 | parser.add_option( 249 | "--username", 250 | action = "append", 251 | dest = "filter_username", 252 | default = [], 253 | help = "Limit display to entries with this username. If given multiple times then displayed entries must match at least one of the usernames. ", 254 | ) 255 | log(10, "Parsing args") 256 | (opts, args) = parser.parse_args() 257 | 258 | if opts.debug: 259 | logger.setLevel(logging.DEBUG) 260 | pws.log.setLevel(logging.DEBUG) 261 | logging.getLogger("psafe.lib.record").setLevel(logging.DEBUG) 262 | logging.getLogger("psafe.lib.header").setLevel(logging.DEBUG) 263 | 264 | # Handle filename as first arg 265 | if not opts.filename: 266 | if len(args) == 1: 267 | opts.filename = args[0] 268 | else: 269 | log(50, "No filename was given") 270 | parser.error("No filename specified. ") 271 | sys.exit(2) 272 | 273 | # FIXME: Add tests to make sure that 274 | # - File exists (reading) 275 | # - File doesn't exist but can create it (creating) 276 | # - Can write the file if not readonly 277 | 278 | if not (opts.xml or opts.csv or opts.display): 279 | opts.display = True 280 | log(10, "No output format given. Defaulting to display.") 281 | 282 | if len(opts.password) == 0: 283 | # FIXME: Add in support for prompting for a password. 284 | raise NotImplementedError 285 | 286 | if opts.readonly: 287 | mode = "RO" 288 | else: 289 | mode = "RW" 290 | log(10, "Set mode to %r", mode) 291 | 292 | psafe = None 293 | for passwd in opts.password: 294 | try: 295 | log(10, "Trying password %r", passwd) 296 | psafe = pws.PWSafe3( 297 | filename = opts.filename, 298 | password = passwd, 299 | mode = mode, 300 | ) 301 | log(10, "Got psafe object %r", psafe) 302 | break 303 | except pws.PasswordError: 304 | log(10, "Password error - Trying next password if any") 305 | continue 306 | 307 | if not psafe: 308 | log(50, "No valid password was found. ") 309 | 310 | to_display = [] 311 | for entry in psafe.records: 312 | if show_entry(entry, opts): 313 | to_display.append(entry) 314 | 315 | if opts.xml: 316 | print display_xml(to_display, opts) 317 | elif opts.csv: 318 | print display_csv(to_display, opts) 319 | elif opts.display: 320 | print display_display(to_display, opts) 321 | 322 | -------------------------------------------------------------------------------- /pwsafecli/pwsafecli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # This file is part of PyPWSafe. 4 | # 5 | # PyPWSafe is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # PyPWSafe is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 17 | #=============================================================================== 18 | 19 | import datetime 20 | from getpass import getpass 21 | import logging, logging.config 22 | from optparse import make_option, OptionParser, OptionGroup 23 | from socket import getfqdn 24 | import string 25 | import sys 26 | import time 27 | from uuid import UUID 28 | 29 | # simplify the naming 30 | Record = None 31 | PWSafe3 = None 32 | 33 | class PWSafeCLIError(Exception): 34 | pass 35 | 36 | class PWSafeCLIValidationError(Exception): 37 | pass 38 | 39 | VALID_ATTRIBUTES = ["group", "title", "username", "password", "UUID", "note", "created", "PasswordModified", 40 | "EntryModified", "LastAccess", "expires", "email", "URL", "AutoType"] 41 | 42 | def get_record_attr(record, attr): 43 | if not attr[0].isupper(): 44 | attr = attr.title() 45 | bound_method = getattr(record, "get%s" % attr) 46 | return bound_method() 47 | 48 | def match_valid(record, **params): 49 | if not params: 50 | return False 51 | 52 | valid = False 53 | 54 | for key, value in params.items(): 55 | if value is None: 56 | continue 57 | valid = get_record_attr(record, key) == value 58 | if not valid: 59 | return False 60 | 61 | return valid 62 | 63 | def get_matching_records(psafe, **params): # pragma: no cover 64 | return [ r for r in psafe.records if match_valid(r, **params) ] 65 | 66 | def new_safe(filename, password, username = None, 67 | dbname = None, dbdesc = None): # pragma: no cover 68 | safe = PWSafe3(filename = filename, password = password, mode = "RW") 69 | 70 | # Set details 71 | safe.setVersion() 72 | safe.setTimeStampOfLastSave(datetime.datetime.now()) 73 | safe.setUUID() 74 | safe.setLastSaveApp('psafecli') 75 | 76 | if username: 77 | safe.setLastSaveUser(username) 78 | 79 | try: 80 | safe.setLastSaveHost(getfqdn()) 81 | except: 82 | pass 83 | 84 | if dbname: 85 | safe.setDbName(dbname) 86 | if dbdesc: 87 | safe.setDbDesc(dbdesc) 88 | 89 | safe.save() 90 | 91 | return safe 92 | 93 | def add_or_update_record(psafe, record, options): # pragma: no cover 94 | """ Adds an entry to the given psafe. Update if it already exists. Reloads the psafe data once complete. 95 | """ 96 | now = datetime.datetime.now() 97 | 98 | if record is None: 99 | record = Record() 100 | record.setCreated(now) 101 | else: 102 | record.setEntryModified(now) 103 | 104 | if options.username: 105 | record.setUsername(options.username) 106 | 107 | if options.password: 108 | record.setPassword(options.password) 109 | 110 | record.setLastAccess(now) 111 | record.setPasswordModified(now) 112 | 113 | if options.group: 114 | record.setGroup(options.group) 115 | 116 | if options.title: 117 | record.setTitle(options.title) 118 | 119 | if options.UUID: 120 | record.setUUID(options.UUID) 121 | 122 | if options.expires: 123 | record.setExpires(options.expires) 124 | 125 | if options.url: 126 | record.setURL(options.url) 127 | 128 | if options.email: 129 | record.setEmail(options.email) 130 | 131 | psafe.records.append(record) 132 | psafe.save() 133 | return record.getUUID() 134 | 135 | def collect_record_options(options): 136 | collected = {} 137 | potentials = ["group", "title", "username", "UUID"] 138 | for item in potentials: 139 | value = getattr(options, item) 140 | if value is not None: 141 | collected[item] = value 142 | return collected 143 | 144 | def show_records(records, attributes): # pragma: no cover 145 | if not attributes: 146 | # show all attributes 147 | attributes = VALID_ATTRIBUTES 148 | 149 | for record in records: 150 | print "[" 151 | for i in attributes: 152 | attr = i 153 | if not i[0].isupper(): 154 | attr = attr.title() 155 | print " %s: %s" % (attr, get_record_attr(record, i)) 156 | print "]" 157 | 158 | def get_safe(filename, password): # pragma: no cover 159 | safe = None 160 | 161 | try: 162 | safe = PWSafe3(filename = filename, 163 | password = password, 164 | mode = "RW") 165 | except pypwsafe.errors.PasswordError: 166 | raise PWSafeCLIError("Invalid password for safe") 167 | 168 | return safe 169 | 170 | class Locked(object): # pragma: no cover 171 | def __init__(self, lock): 172 | self.lock = lock 173 | 174 | def __enter__(self): 175 | self.lock.lock() 176 | 177 | def __exit__(self, type, value, tb): 178 | self.lock.unlock() 179 | 180 | def add_validator(options): 181 | if options.title is None: 182 | raise PWSafeCLIValidationError("--title must be specified") 183 | if options.password is None: 184 | raise PWSafeCLIValidationError("--password must be specified") 185 | 186 | def add_action(options): # pragma: no cover 187 | safe = get_safe(options.filename, options.safe_password) 188 | with Locked(safe): 189 | result = add_or_update_record(safe, None, options) 190 | if options.verbose: 191 | print result 192 | 193 | def delete_validator(options): 194 | if options.UUID is None: 195 | raise PWSafeCLIValidationError("--uuid must be specified") 196 | 197 | def delete_action(options): # pragma: no cover 198 | safe = get_safe(options.filename, options.safe_password) 199 | 200 | with Locked(safe): 201 | records = get_matching_records(safe, {"UUID": options.UUID}) 202 | count = len(records) 203 | if count == 0: 204 | raise PWSafeCLIError("no matching records found") 205 | elif count > 1: 206 | raise NotImplementedError("implement multiple record choice") 207 | 208 | safe.records.remove(records[0]) 209 | safe.save() 210 | 211 | def display_validator(option): 212 | attrs = option.split(',') 213 | try: 214 | pos = attrs.index("uuid") 215 | attrs[pos] = "UUID" 216 | except ValueError: 217 | pass 218 | 219 | unsupported = [ field for field in attrs if not is_valid_field_name(field) ] 220 | if unsupported: 221 | raise PWSafeCLIValidationError("unsupport display fields: %s" % unsupported) 222 | 223 | def is_valid_field_name(field): 224 | if field in VALID_ATTRIBUTES: 225 | return True 226 | return False 227 | 228 | def dump_validator(options): # pragma: no cover 229 | if options.display: 230 | display_validator(options.display) 231 | 232 | def dump_action(options): # pragma: no cover 233 | safe = get_safe(options.filename, options.safe_password) 234 | 235 | with Locked(safe): 236 | if not safe.records: 237 | raise PWSafeCLIError("No records") 238 | 239 | show_records(safe.records, options.display) 240 | 241 | def get_validator(options): 242 | if not any([ getattr(options, attr) for attr in ("group", "title", "username", "UUID")]): 243 | raise PWSafeCLIValidationError("one of --group, --title, --username or --uuid must be provided") 244 | 245 | if options.display: 246 | display_validator(options.display) 247 | 248 | def get_action(options): # pragma: no cover 249 | record_options = collect_record_options(options) 250 | 251 | safe = get_safe(options.filename, options.safe_password) 252 | 253 | with Locked(safe): 254 | records = get_matching_records(safe, **record_options) 255 | if not records: 256 | raise PWSafeCLIError("No records matching %s found" % record_options) 257 | 258 | show_records(records, options.display) 259 | 260 | def init_validator(options): 261 | pass 262 | 263 | def init_action(options): # pragma: no cover 264 | safe = new_safe(options.filename, options.safe_password, options.username, 265 | options.dbname, options.dbdesc) 266 | 267 | def update_validator(options): 268 | if not options.UUID: 269 | raise PWSafeCLIValidationError("must provide --uuid") 270 | 271 | def update_action(options): # pragma: no cover 272 | record_options = collect_record_options(options) 273 | 274 | safe = get_safe(options.filename, options.safe_password) 275 | 276 | with Locked(safe): 277 | records = get_matching_records(safe, **record_options) 278 | count = len(records) 279 | if count == 0: 280 | raise PWSafeCLIError("No records matching %s found" % record_options) 281 | elif count > 1: 282 | raise NotImplementedError("implement multiple record choice") 283 | add_or_update_record(safe, records[0], options) 284 | 285 | usage_message = """ 286 | Usage: psafecli [add|delete|get|init|update] 287 | 288 | Run help for a subcommand for more options. 289 | """ 290 | 291 | def makeArgParser(): 292 | parsers = {} 293 | 294 | base_options = [ 295 | make_option("-f", "--file", dest="filename", 296 | help="use FILE as PWSafe container", metavar="FILE"), 297 | make_option("--verbose", action="store_true"), 298 | make_option("--debug", action="store_true"), 299 | ] 300 | 301 | common_record_options = [ 302 | make_option("--email", help="E-mail for contact person of Record"), 303 | make_option("--group", help="group of Record"), 304 | make_option("--title", help="title of Record"), 305 | make_option("--username", help="user of Record"), 306 | make_option("--uuid", dest="UUID", help="UUID of Record"), 307 | ] 308 | 309 | record_options = [ 310 | make_option("--expires", help="Date Record expires ex. 2014-07-03 15:30"), 311 | make_option("--password", help="password of Record (not the Safe itself)"), 312 | make_option("--url", help="URL for Record"), 313 | ] 314 | 315 | display_option = make_option("--display", help="comma separated list of record attributes to display from this list: %s" % VALID_ATTRIBUTES) 316 | 317 | parser = OptionParser(option_list=base_options, 318 | usage="psafecli init [options]") 319 | parser.add_option("--dbname", help="Name of new DB") 320 | parser.add_option("--dbdesc", help="Description of new DB") 321 | parser.add_option("--username", help="user of Safe") 322 | parsers["init"] = parser 323 | 324 | parser = OptionParser(option_list=(base_options + [display_option]), 325 | usage="psafecli dump [options]") 326 | parsers["dump"] = parser 327 | 328 | parser = OptionParser(option_list=(base_options + common_record_options + [display_option]), 329 | usage="psafecli get [options]") 330 | parsers["get"] = parser 331 | 332 | parser = OptionParser(option_list=(base_options + common_record_options + record_options), usage="psafecli add [options]") 333 | parsers["add"] = parser 334 | 335 | parser = OptionParser(option_list = base_options, 336 | usage="psafecli delete [options]") 337 | parser.add_option("--uuid", dest="UUID", help="UUID of Record") 338 | parsers["delete"] = parser 339 | 340 | parser = OptionParser(option_list=(base_options + common_record_options + record_options), usage="psafecli update [options]") 341 | parsers["update"] = parser 342 | 343 | return parsers 344 | 345 | def parse_commandline(parsers, argv): 346 | if len(argv) == 1: 347 | raise PWSafeCLIValidationError("must specify a command to run") 348 | elif argv[1].startswith('-'): 349 | raise PWSafeCLIValidationError(usage_message) 350 | 351 | action = argv[1] 352 | 353 | if action not in parsers.keys(): 354 | raise PWSafeCLIValidationError("unknown action: %s" % action) 355 | 356 | parser = parsers[action] 357 | 358 | (options, args) = parser.parse_args(argv[2:]) 359 | 360 | options.action = action 361 | 362 | if options.debug: 363 | logger = logging.getLogger('psafe') 364 | logger.setLevel(logging.DEBUG) 365 | 366 | if options.filename is None: 367 | parser.error("Must provide filename") 368 | 369 | if parser.has_option("--group") and options.group: 370 | options.group = options.group.split('.') 371 | 372 | if parser.has_option("--uuid") and options.UUID: 373 | options.UUID = UUID(options.UUID) 374 | 375 | if parser.has_option("--expires") and options.expires: 376 | try: 377 | options.expires = time.strptime(options.expires, "%Y-%m-%d %H:%M") 378 | except ValueError: 379 | raise PWSafeCLIValidationError("date entered does not match %Y-%m-%d %H:%M format") 380 | 381 | options.safe_password = None 382 | 383 | return options 384 | 385 | def main(options): # pragma: no cover 386 | actions = { "add": (add_validator, add_action), 387 | "delete": (delete_validator, delete_action), 388 | "dump": (dump_validator, dump_action), 389 | "get": (get_validator, get_action), 390 | "init": (init_validator, init_action), 391 | "update": (update_validator, update_action), 392 | } 393 | 394 | validator, func = actions.get(options.action, None) 395 | if not func: 396 | raise PWSafeCLIValidationError("%s is not supported\n" % options.action) 397 | 398 | if validator: 399 | validator(options) 400 | 401 | if not options.safe_password: 402 | options.safe_password = getpass("Enter the password for PWSafe3 file: %s\n> " % options.filename) 403 | 404 | func(options) 405 | 406 | if __name__ == "__main__": # pragma: no cover 407 | logging.basicConfig(level = logging.WARNING, 408 | format = '%(asctime)s %(levelname)s %(message)s', 409 | stream=sys.stderr) 410 | logger = logging.getLogger('psafe') 411 | logger.setLevel(logging.WARNING) 412 | 413 | import pypwsafe 414 | Record = pypwsafe.Record 415 | PWSafe3 = pypwsafe.PWSafe3 416 | 417 | parsers = makeArgParser() 418 | 419 | try: 420 | options = parse_commandline(parsers, sys.argv) 421 | 422 | main(options) 423 | except PWSafeCLIValidationError, e: 424 | sys.stderr.write("%s\n" % e) 425 | sys.exit(1) 426 | except PWSafeCLIError, e: 427 | sys.stderr.write("%s\n" % e) 428 | sys.exit(1) 429 | -------------------------------------------------------------------------------- /src/pypwsafe/consts.py: -------------------------------------------------------------------------------- 1 | #=============================================================================== 2 | # SYMANTEC: Copyright (C) 2009-2011 Symantec Corporation. All rights reserved. 3 | # 4 | # This file is part of PyPWSafe. 5 | # 6 | # PyPWSafe is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # PyPWSafe is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 18 | #=============================================================================== 19 | ''' Useful constants 20 | 21 | @author: Paulson McIntyre 22 | @license: GPLv2 23 | @version: 0.1 24 | 25 | Created on Oct 27, 2010 26 | ''' 27 | # Default special chars 28 | DEFAULT_SPECIAL_CHARS = "+-=_@#$%^&;:,.<>/~\\[](){}?!|" 29 | DEFAULT_EASY_SPECIAL_CHARS = "+-=_@#$%^<>/~\\?" 30 | 31 | # Configuration options 32 | 33 | # Double click and shift double clickactions 34 | click_actions = dict( 35 | DoubleClickCopyPassword = 0, 36 | DoubleClickViewEdit = 1, 37 | DoubleClickAutoType = 2, 38 | DoubleClickBrowse = 3, 39 | DoubleClickCopyNotes = 4, 40 | DoubleClickCopyUsername = 5, 41 | DoubleClickCopyPasswordMinimize = 6, 42 | DoubleClickBrowsePlus = 7, 43 | DoubleClickRun = 8, 44 | DoubleClickSendEmail = 9, 45 | ) 46 | 47 | # Configuration Statics 48 | ptApplication = 0 49 | ptDatabase = 1 50 | ptObsolete = 2 51 | 52 | # File format version format 53 | version_map = { 54 | "PasswordSafe V3.01":0x0300, 55 | "PasswordSafe V3.03":0x0301, 56 | "PasswordSafe V3.09":0x0302, 57 | "PasswordSafe V3.12":0x0303, 58 | "PasswordSafe V3.13":0x0304, 59 | "PasswordSafe V3.14":0x0305, 60 | "PasswordSafe V3.19":0x0306, 61 | "PasswordSafe V3.22":0x0307, 62 | "PasswordSafe V3.25":0x0308, 63 | "PasswordSafe V3.26":0x0309, 64 | "PasswordSafe V3.28":0x030A, 65 | "PasswordSafe V3.29":0x030B, 66 | "PasswordSafe V3.29Y":0x030C, 67 | } 68 | 69 | # Bools 70 | conf_bools = { 71 | 'AlwaysOnTop':{ 72 | 'default':False, 73 | 'type':ptApplication, 74 | 'name':'AlwaysOnTop', 75 | 'index':0 76 | }, 77 | 78 | 'ShowPWDefault':{ 79 | 'default':False, 80 | 'type':ptDatabase, 81 | 'name':'ShowPWDefault', 82 | 'index':1 83 | }, 84 | 85 | 'ShowPasswordInTree':{ 86 | 'default':False, 87 | 'type':ptDatabase, 88 | 'name':'ShowPasswordInTree', 89 | 'index':2 90 | }, 91 | 92 | 'SortAscending':{ 93 | 'default':True, 94 | 'type':ptDatabase, 95 | 'name':'SortAscending', 96 | 'index':3 97 | }, 98 | 99 | 'UseDefaultUser':{ 100 | 'default':False, 101 | 'type':ptDatabase, 102 | 'name':'UseDefaultUser', 103 | 'index':4 104 | }, 105 | 106 | 'SaveImmediately':{ 107 | 'default':True, 108 | 'type':ptDatabase, 109 | 'name':'SaveImmediately', 110 | 'index':5 111 | }, 112 | 113 | 'PWUseLowercase':{ 114 | 'default':True, 115 | 'type':ptDatabase, 116 | 'name':'PWUseLowercase', 117 | 'index':6 118 | }, 119 | 120 | 'PWUseUppercase':{ 121 | 'default':True, 122 | 'type':ptDatabase, 123 | 'name':'PWUseUppercase', 124 | 'index':7 125 | }, 126 | 127 | 'PWUseDigits':{ 128 | 'default':True, 129 | 'type':ptDatabase, 130 | 'name':'PWUseDigits', 131 | 'index':8 132 | }, 133 | 134 | 'PWUseSymbols':{ 135 | 'default':False, 136 | 'type':ptDatabase, 137 | 'name':'PWUseSymbols', 138 | 'index':9 139 | }, 140 | 141 | 'PWUseHexDigits':{ 142 | 'default':False, 143 | 'type':ptDatabase, 144 | 'name':'PWUseHexDigits', 145 | 'index':10 146 | }, 147 | 148 | 'PWUseEasyVision':{ 149 | 'default':False, 150 | 'type':ptDatabase, 151 | 'name':'PWUseEasyVision', 152 | 'index':11 153 | }, 154 | 155 | 'dontaskquestion':{ 156 | 'default':False, 157 | 'type':ptApplication, 158 | 'name':'dontaskquestion', 159 | 'index':12 160 | }, 161 | 162 | 'deletequestion':{ 163 | 'default':False, 164 | 'type':ptApplication, 165 | 'name':'deletequestion', 166 | 'index':13 167 | }, 168 | 169 | 'DCShowsPassword':{ 170 | 'default':False, 171 | 'type':ptApplication, 172 | 'name':'DCShowsPassword', 173 | 'index':14 174 | }, 175 | 176 | 'DontAskMinimizeClearYesNo':{ 177 | 'default':True, 178 | 'type':ptObsolete, 179 | 'name':'DontAskMinimizeClearYesNo', 180 | 'index':15 181 | }, 182 | 183 | 'DatabaseClear':{ 184 | 'default':False, 185 | 'type':ptApplication, 186 | 'name':'DatabaseClear', 187 | 'index':16 188 | }, 189 | 190 | 'DontAskSaveMinimize':{ 191 | 'default':False, 192 | 'type':ptObsolete, 193 | 'name':'DontAskSaveMinimize', 194 | 'index':17 195 | }, 196 | 197 | 'QuerySetDef':{ 198 | 'default':True, 199 | 'type':ptApplication, 200 | 'name':'QuerySetDef', 201 | 'index':18 202 | }, 203 | 204 | 'UseNewToolbar':{ 205 | 'default':True, 206 | 'type':ptApplication, 207 | 'name':'UseNewToolbar', 208 | 'index':19 209 | }, 210 | 211 | 'UseSystemTray':{ 212 | 'default':True, 213 | 'type':ptApplication, 214 | 'name':'UseSystemTray', 215 | 'index':20 216 | }, 217 | 218 | 'LockOnWindowLock':{ 219 | 'default':True, 220 | 'type':ptApplication, 221 | 'name':'LockOnWindowLock', 222 | 'index':21 223 | }, 224 | 225 | 'LockOnIdleTimeout':{ 226 | 'default':True, 227 | 'type':ptObsolete, 228 | 'name':'LockOnIdleTimeout', 229 | 'index':22 230 | }, 231 | 232 | 'EscExits':{ 233 | 'default':True, 234 | 'type':ptApplication, 235 | 'name':'EscExits', 236 | 'index':23 237 | }, 238 | 239 | 'IsUTF8':{ 240 | 'default':False, 241 | 'type':ptDatabase, 242 | 'name':'IsUTF8', 243 | 'index':24 244 | }, 245 | 246 | 'HotKeyEnabled':{ 247 | 'default':False, 248 | 'type':ptApplication, 249 | 'name':'HotKeyEnabled', 250 | 'index':25 251 | }, 252 | 253 | 'MRUOnFileMenu':{ 254 | 'default':True, 255 | 'type':ptApplication, 256 | 'name':'MRUOnFileMenu', 257 | 'index':26 258 | }, 259 | 260 | 'DisplayExpandedAddEditDlg':{ 261 | 'default':True, 262 | 'type':ptObsolete, 263 | 'name':'DisplayExpandedAddEditDlg', 264 | 'index':27 265 | }, 266 | 267 | 'MaintainDateTimeStamps':{ 268 | 'default':False, 269 | 'type':ptDatabase, 270 | 'name':'MaintainDateTimeStamps', 271 | 'index':28 272 | }, 273 | 274 | 'SavePasswordHistory':{ 275 | 'default':False, 276 | 'type':ptDatabase, 277 | 'name':'SavePasswordHistory', 278 | 'index':29 279 | }, 280 | 281 | 'FindWraps':{ 282 | 'default':False, 283 | 'type':ptObsolete, 284 | 'name':'FindWraps', 285 | 'index':30 286 | }, 287 | 288 | 'ShowNotesDefault':{ 289 | 'default':False, 290 | 'type':ptDatabase, 291 | 'name':'ShowNotesDefault', 292 | 'index':31 293 | }, 294 | 295 | 'BackupBeforeEverySave':{ 296 | 'default':True, 297 | 'type':ptApplication, 298 | 'name':'BackupBeforeEverySave', 299 | 'index':32 300 | }, 301 | 302 | 'PreExpiryWarn':{ 303 | 'default':False, 304 | 'type':ptApplication, 305 | 'name':'PreExpiryWarn', 306 | 'index':33 307 | }, 308 | 309 | 'ExplorerTypeTree':{ 310 | 'default':False, 311 | 'type':ptApplication, 312 | 'name':'ExplorerTypeTree', 313 | 'index':34 314 | }, 315 | 316 | 'ListViewGridLines':{ 317 | 'default':False, 318 | 'type':ptApplication, 319 | 'name':'ListViewGridLines', 320 | 'index':35 321 | }, 322 | 323 | 'MinimizeOnAutotype':{ 324 | 'default':True, 325 | 'type':ptApplication, 326 | 'name':'MinimizeOnAutotype', 327 | 'index':36 328 | }, 329 | 330 | 'ShowUsernameInTree':{ 331 | 'default':True, 332 | 'type':ptDatabase, 333 | 'name':'ShowUsernameInTree', 334 | 'index':37 335 | }, 336 | 337 | 'PWMakePronounceable':{ 338 | 'default':False, 339 | 'type':ptDatabase, 340 | 'name':'PWMakePronounceable', 341 | 'index':38 342 | }, 343 | 344 | 'ClearClipoardOnMinimize':{ 345 | 'default':True, 346 | 'type':ptObsolete, 347 | 'name':'ClearClipoardOnMinimize', 348 | 'index':39 349 | }, 350 | 351 | 'ClearClipoardOneExit':{ 352 | 'default':True, 353 | 'type':ptObsolete, 354 | 'name':'ClearClipoardOneExit', 355 | 'index':40 356 | }, 357 | 358 | 'ShowToolbar':{ 359 | 'default':True, 360 | 'type':ptApplication, 361 | 'name':'ShowToolbar', 362 | 'index':41 363 | }, 364 | 365 | 'ShowNotesAsToolTipsInViews':{ 366 | 'default':False, 367 | 'type':ptApplication, 368 | 'name':'ShowNotesAsToolTipsInViews', 369 | 'index':42 370 | }, 371 | 372 | 'DefaultOpenRO':{ 373 | 'default':False, 374 | 'type':ptApplication, 375 | 'name':'DefaultOpenRO', 376 | 'index':43 377 | }, 378 | 379 | 'MultipleInstances':{ 380 | 'default':True, 381 | 'type':ptApplication, 382 | 'name':'MultipleInstances', 383 | 'index':44 384 | }, 385 | 386 | 'ShowDragbar':{ 387 | 'default':True, 388 | 'type':ptApplication, 389 | 'name':'ShowDragbar', 390 | 'index':45 391 | }, 392 | 393 | 'ClearClipboardOnMinimize':{ 394 | 'default':True, 395 | 'type':ptApplication, 396 | 'name':'ClearClipboardOnMinimize', 397 | 'index':46 398 | }, 399 | 400 | 'ClearClipboardOnExit':{ 401 | 'default':True, 402 | 'type':ptApplication, 403 | 'name':'ClearClipboardOnExit', 404 | 'index':47 405 | }, 406 | 407 | 'ShowFindToolBarOnOpen':{ 408 | 'default':False, 409 | 'type':ptApplication, 410 | 'name':'ShowFindToolBarOnOpen', 411 | 'index':48 412 | }, 413 | 414 | 'NotesWordWrap':{ 415 | 'default':False, 416 | 'type':ptApplication, 417 | 'name':'NotesWordWrap', 418 | 'index':49 419 | }, 420 | 421 | 'LockDBOnIdleTimeout':{ 422 | 'default':True, 423 | 'type':ptDatabase, 424 | 'name':'LockDBOnIdleTimeout', 425 | 'index':50 426 | }, 427 | 428 | 'HighlightChanges':{ 429 | 'default':True, 430 | 'type':ptApplication, 431 | 'name':'HighlightChanges', 432 | 'index':51 433 | }, 434 | 435 | 'HideSystemTray':{ 436 | 'default':False, 437 | 'type':ptApplication, 438 | 'name':'HideSystemTray', 439 | 'index':52 440 | }, 441 | 442 | 'UsePrimarySelectionForClipboard':{ 443 | 'default':False, 444 | 'type':ptApplication, 445 | 'name':'UsePrimarySelectionForClipboard', 446 | 'index':53 447 | }, 448 | 449 | 'CopyPasswordWhenBrowseToURL':{ 450 | 'default':False, 451 | 'type':ptDatabase, 452 | 'name':'CopyPasswordWhenBrowseToURL', 453 | 'index':54 454 | }, 455 | 456 | } 457 | 458 | # Ints 459 | conf_ints = { 460 | 'column1width':{ 461 | 'name':'column1width', 462 | 'default':65535, 463 | 'type':ptApplication, 464 | 'min':-1, 465 | 'max':-1, 466 | 'index':0, 467 | }, 468 | 469 | 'column2width':{ 470 | 'name':'column2width', 471 | 'default':65535, 472 | 'type':ptApplication, 473 | 'min':-1, 474 | 'max':-1, 475 | 'index':1, 476 | }, 477 | 478 | 'column3width':{ 479 | 'name':'column3width', 480 | 'default':65535, 481 | 'type':ptApplication, 482 | 'min':-1, 483 | 'max':-1, 484 | 'index':2, 485 | }, 486 | 487 | 'column4width':{ 488 | 'name':'column4width', 489 | 'default':65535, 490 | 'type':ptApplication, 491 | 'min':-1, 492 | 'max':-1, 493 | 'index':3, 494 | }, 495 | 496 | 'sortedcolumn':{ 497 | 'name':'sortedcolumn', 498 | 'default':0, 499 | 'type':ptApplication, 500 | 'min':-1, 501 | 'max':-1, 502 | 'index':4, 503 | }, 504 | 505 | 'PWDefaultLength':{ 506 | 'name':'PWDefaultLength', 507 | 'default':8, 508 | 'type':ptDatabase, 509 | 'min':-1, 510 | 'max':-1, 511 | 'index':5, 512 | }, 513 | 514 | 'maxmruitems':{ 515 | 'name':'maxmruitems', 516 | 'default':4, 517 | 'type':ptApplication, 518 | 'min':-1, 519 | 'max':-1, 520 | 'index':6, 521 | }, 522 | 523 | 'IdleTimeout':{ 524 | 'name':'IdleTimeout', 525 | 'default':5, 526 | 'type':ptDatabase, 527 | 'min':-1, 528 | 'max':-1, 529 | 'index':7, 530 | }, 531 | 532 | 'DoubleClickAction':{ 533 | 'name':'DoubleClickAction', 534 | 'default':0, 535 | 'type':ptApplication, 536 | 'min':-1, 537 | 'max':-1, 538 | 'index':8, 539 | }, 540 | 541 | 'HotKey':{ 542 | 'name':'HotKey', 543 | 'default':0, 544 | 'type':ptApplication, 545 | 'min':-1, 546 | 'max':-1, 547 | 'index':9, 548 | }, 549 | 550 | 'MaxREItems':{ 551 | 'name':'MaxREItems', 552 | 'default':25, 553 | 'type':ptApplication, 554 | 'min':-1, 555 | 'max':-1, 556 | 'index':10, 557 | }, 558 | 559 | 'TreeDisplayStatusAtOpen':{ 560 | 'name':'TreeDisplayStatusAtOpen', 561 | 'default':0, 562 | 'type':ptDatabase, 563 | 'min':-1, 564 | 'max':-1, 565 | 'index':11, 566 | }, 567 | 568 | 'NumPWHistoryDefault':{ 569 | 'name':'NumPWHistoryDefault', 570 | 'default':3, 571 | 'type':ptDatabase, 572 | 'min':-1, 573 | 'max':-1, 574 | 'index':12, 575 | }, 576 | 577 | 'BackupSuffix':{ 578 | 'name':'BackupSuffix', 579 | 'default':0, 580 | 'type':ptApplication, 581 | 'min':-1, 582 | 'max':-1, 583 | 'index':13, 584 | }, 585 | 586 | 'BackupMaxIncremented':{ 587 | 'name':'BackupMaxIncremented', 588 | 'default':1, 589 | 'type':ptApplication, 590 | 'min':-1, 591 | 'max':-1, 592 | 'index':14, 593 | }, 594 | 595 | 'PreExpiryWarnDays':{ 596 | 'name':'PreExpiryWarnDays', 597 | 'default':1, 598 | 'type':ptApplication, 599 | 'min':-1, 600 | 'max':-1, 601 | 'index':15, 602 | }, 603 | 604 | 'ClosedTrayIconColour':{ 605 | 'name':'ClosedTrayIconColour', 606 | 'default':0, 607 | 'type':ptApplication, 608 | 'min':-1, 609 | 'max':-1, 610 | 'index':16, 611 | }, 612 | 613 | 'PWDigitMinLength':{ 614 | 'name':'PWDigitMinLength', 615 | 'default':0, 616 | 'type':ptDatabase, 617 | 'min':-1, 618 | 'max':-1, 619 | 'index':17, 620 | }, 621 | 622 | 'PWLowercaseMinLength':{ 623 | 'name':'PWLowercaseMinLength', 624 | 'default':0, 625 | 'type':ptDatabase, 626 | 'min':-1, 627 | 'max':-1, 628 | 'index':18, 629 | }, 630 | 631 | 'PWSymbolMinLength':{ 632 | 'name':'PWSymbolMinLength', 633 | 'default':0, 634 | 'type':ptDatabase, 635 | 'min':-1, 636 | 'max':-1, 637 | 'index':19, 638 | }, 639 | 640 | 'PWUppercaseMinLength':{ 641 | 'name':'PWUppercaseMinLength', 642 | 'default':0, 643 | 'type':ptDatabase, 644 | 'min':-1, 645 | 'max':-1, 646 | 'index':20, 647 | }, 648 | 649 | 'OptShortcutColumnWidth':{ 650 | 'name':'OptShortcutColumnWidth', 651 | 'default':92, 652 | 'type':ptApplication, 653 | 'min':-1, 654 | 'max':-1, 655 | 'index':21, 656 | }, 657 | 658 | 'ShiftDoubleClickAction':{ 659 | 'name':'ShiftDoubleClickAction', 660 | 'default':click_actions['DoubleClickCopyUsername'], 661 | 'type':ptApplication, 662 | 'min':click_actions['DoubleClickCopyPassword'], 663 | 'max':click_actions['DoubleClickSendEmail'], 664 | 'index':22, 665 | }, 666 | } 667 | 668 | # Strings 669 | conf_strs = { 670 | 'currentbackup':{ 671 | 'name':'currentbackup', 672 | 'default':'', 673 | 'type':ptApplication, 674 | 'index':0, 675 | }, 676 | 677 | 'currentfile':{ 678 | 'name':'currentfile', 679 | 'default':'', 680 | 'type':ptApplication, 681 | 'index':1, 682 | }, 683 | 684 | 'lastview':{ 685 | 'name':'lastview', 686 | 'default':'tree', 687 | 'type':ptApplication, 688 | 'index':2, 689 | }, 690 | 691 | 'DefaultUsername':{ 692 | 'name':'DefaultUsername', 693 | 'default':'', 694 | 'type':ptDatabase, 695 | 'index':3, 696 | }, 697 | 698 | 'treefont':{ 699 | 'name':'treefont', 700 | 'default':'', 701 | 'type':ptApplication, 702 | 'index':4, 703 | }, 704 | 705 | 'BackupPrefixValue':{ 706 | 'name':'BackupPrefixValue', 707 | 'default':'', 708 | 'type':ptApplication, 709 | 'index':5, 710 | }, 711 | 712 | 'BackupDir':{ 713 | 'name':'BackupDir', 714 | 'default':'', 715 | 'type':ptApplication, 716 | 'index':6, 717 | }, 718 | 719 | 'AltBrowser':{ 720 | 'name':'AltBrowser', 721 | 'default':'', 722 | 'type':ptApplication, 723 | 'index':7, 724 | }, 725 | 726 | 'ListColumns':{ 727 | 'name':'ListColumns', 728 | 'default':'', 729 | 'type':ptApplication, 730 | 'index':8, 731 | }, 732 | 733 | 'ColumnWidths':{ 734 | 'name':'ColumnWidths', 735 | 'default':'', 736 | 'type':ptApplication, 737 | 'index':9, 738 | }, 739 | 740 | 'DefaultAutotypeString':{ 741 | 'name':'DefaultAutotypeString', 742 | 'default':'', 743 | 'type':ptDatabase, 744 | 'index':10, 745 | }, 746 | 747 | 'AltBrowserCmdLineParms':{ 748 | 'name':'AltBrowserCmdLineParms', 749 | 'default':'', 750 | 'type':ptApplication, 751 | 'index':11, 752 | }, 753 | 754 | 'MainToolBarButtons':{ 755 | 'name':'MainToolBarButtons', 756 | 'default':'', 757 | 'type':ptApplication, 758 | 'index':12, 759 | }, 760 | 761 | 'PasswordFont':{ 762 | 'name':'PasswordFont', 763 | 'default':'', 764 | 'type':ptApplication, 765 | 'index':13, 766 | }, 767 | 768 | 'TreeListSampleText':{ 769 | 'name':'TreeListSampleText', 770 | 'default':'AaBbYyZz 0O1IlL', 771 | 'type':ptApplication, 772 | 'index':14, 773 | }, 774 | 775 | 'PswdSampleText':{ 776 | 'name':'PswdSampleText', 777 | 'default':'AaBbYyZz 0O1IlL', 778 | 'type':ptApplication, 779 | 'index':15, 780 | }, 781 | 782 | 'LastUsedKeyboard':{ 783 | 'name':'LastUsedKeyboard', 784 | 'default':'', 785 | 'type':ptApplication, 786 | 'index':16, 787 | }, 788 | 789 | 'VKeyboardFontName':{ 790 | 'name':'VKeyboardFontName', 791 | 'default':'', 792 | 'type':ptApplication, 793 | 'index':17, 794 | }, 795 | 796 | 'VKSampleText':{ 797 | 'name':'VKSampleText', 798 | 'default':'AaBbYyZz 0O1IlL', 799 | 'type':ptApplication, 800 | 'index':18, 801 | }, 802 | 803 | 'AltNotesEditor':{ 804 | 'name':'AltNotesEditor', 805 | 'default':'', 806 | 'type':ptApplication, 807 | 'index':19, 808 | }, 809 | 810 | 'LanguageFile':{ 811 | 'name':'LanguageFile', 812 | 'default':'', 813 | 'type':ptApplication, 814 | 'index':20, 815 | }, 816 | 817 | 'DefaultSymbols':{ 818 | 'name':'DefaultSymbols', 819 | 'default':'', 820 | 'type':ptDatabase, 821 | 'index':21, 822 | }, 823 | 824 | } 825 | 826 | # Type Mappings 827 | conf_types = {} 828 | for name, info in conf_bools.items(): 829 | conf_types[name] = bool 830 | for name, info in conf_ints.items(): 831 | conf_types[name] = int 832 | for name, info in conf_strs.items(): 833 | conf_types[name] = str 834 | 835 | 836 | 837 | -------------------------------------------------------------------------------- /src/pypwsafe/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # SYMANTEC: Copyright (C) 2009-2011 Symantec Corporation. All rights reserved. 4 | # 5 | # This file is part of PyPWSafe. 6 | # 7 | # PyPWSafe is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # PyPWSafe is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 19 | #=============================================================================== 20 | """ Read & write Password Safe v3 files. 21 | 22 | @author: Paulson McIntyre 23 | @license: GPLv2 24 | @version: 0.3 25 | """ 26 | # Lets this lib work from both 2.4 and above 27 | try: 28 | from hashlib import sha256_func # @UnresolvedImport 29 | from hashlib import sha256_mod # @UnresolvedImport 30 | except: 31 | import Crypto.Hash.SHA256 as sha256_mod # @UnresolvedImport @Reimport 32 | from Crypto.Hash.SHA256 import new as sha256_func # @UnresolvedImport @Reimport 33 | from mcrypt import MCRYPT # @UnresolvedImport 34 | from hmac import new as HMAC 35 | from PWSafeV3Headers import * 36 | from PWSafeV3Records import * 37 | from errors import * 38 | import os, os.path 39 | from struct import pack, unpack 40 | import logging, logging.config 41 | import socket 42 | import getpass 43 | import re 44 | 45 | log = logging.getLogger("psafe.lib.init") 46 | log.debug('initing') 47 | from uuid import uuid4 48 | 49 | def stretchkey(passwd, salt, count): 50 | """ 51 | Stretch a key. H(pass+salt) 52 | @param passwd: The password being stretched 53 | @type passwd: string 54 | @param salt: Salt for the password. Should pre-provided random data. 55 | @type salt: string 56 | @param count: The number of times to repeat the stretch function 57 | @type count: int 58 | """ 59 | assert count > 0 60 | # Hash once with both 61 | inithsh = sha256_func() 62 | inithsh.update(passwd) 63 | inithsh.update(salt) 64 | # Expecting it in binary form; NOT HEX FORM 65 | hsh = inithsh.digest() 66 | # Rehash 67 | for i in xrange(count): 68 | t = sha256_func() 69 | t.update(hsh) 70 | hsh = t.digest() 71 | return hsh 72 | 73 | def _findHeader(headers, htype): 74 | for hdr in headers: 75 | if type(hdr) == htype: 76 | return hdr 77 | return None 78 | 79 | def _getHeaderField(headers, htype, ignored = ''): 80 | hdr = _findHeader(headers, htype) 81 | if hdr: 82 | return getattr(hdr, htype.FIELD) 83 | return None 84 | 85 | def _getHeaderFields(headers, htype): 86 | """ For headers that may be there multiple times """ 87 | found = [] 88 | for hdr in headers: 89 | if type(hdr) == htype: 90 | found.append(getattr(hdr, htype.FIELD)) 91 | return found 92 | 93 | def _setHeaderField(headers, htype, value): 94 | hdr = _findHeader(headers, htype) 95 | if hdr: 96 | setattr(hdr, htype.FIELD, value) 97 | return True 98 | return False 99 | 100 | from struct import pack, unpack 101 | class PWSafe3(object): 102 | """ A Password safe object. Allows read/write access to most header fields and records in a psafe object. 103 | """ 104 | 105 | filename = None 106 | """@ivar: Full path to pwsafe 107 | @type filename: string 108 | """ 109 | 110 | password = None 111 | """@ivar: Passsafe password 112 | @type password: string 113 | """ 114 | 115 | fl = None 116 | """@ivar: PWSafe file handle 117 | @type fl: File Handle 118 | """ 119 | 120 | flfull = None 121 | """@ivar: Contents of pwsafe file 122 | @type flfull: string 123 | """ 124 | 125 | pprime = None 126 | """@ivar: Stretched key used in B1 - B4 127 | @type pprime: string 128 | """ 129 | 130 | enckey = None 131 | """@ivar: K; session key for main data block 132 | @type enckey: string 133 | """ 134 | 135 | hshkey = None 136 | """@ivar: L; hmac key 137 | @type hshkey: string 138 | """ 139 | 140 | records = None 141 | """@ivar: List of all records we have 142 | @type records: [Record,...] 143 | """ 144 | 145 | hmacreq = None 146 | """@ivar: List of functions to run to generate hmac. Order matters when reading a file. 147 | @type hmacreq: function 148 | """ 149 | 150 | hmac = None 151 | """@ivar: Originally its the hmac from the file. Should be updated when ever changes are made. 152 | @type hmac: string 153 | """ 154 | 155 | mode = None 156 | """@ivar: Read only or read/write mode. "RO" for read-only or "RW" or read/write. 157 | @type mode: string 158 | """ 159 | 160 | iv = None 161 | """@ivar: Initialization vector used for CBC mode when encrypting / decrypting the header and records. 162 | @type iv: string[16] 163 | """ 164 | 165 | def __init__(self, filename, password, mode = "RW"): 166 | """ 167 | @param filename: The path to the Password Safe file. Will be created if it doesn't already exist. 168 | @type filename: string 169 | @param password: The password to encrypt/decrypt the safe with. 170 | @type password: string 171 | @param mode: Read only or read/write mode. "RO" for read-only or "RW" or read/write. 172 | @type mode: string 173 | """ 174 | log.debug('Creating psafe %s' % repr(filename)) 175 | self.locked = False 176 | filename = os.path.realpath(filename) 177 | psafe_exists = os.access(filename, os.F_OK) 178 | psafe_canwrite = os.access(filename, os.W_OK) 179 | psafe_canwritebase = os.access(os.path.dirname(filename), os.W_OK) 180 | psafe_canread = os.access(filename, os.R_OK) 181 | if psafe_exists and psafe_canread and not psafe_canwrite: 182 | log.debug("Opening RO") 183 | self.mode = "RO" 184 | elif psafe_exists and psafe_canread and psafe_canwrite and mode == "RW": 185 | log.debug("Opening RW") 186 | self.mode = "RW" 187 | elif psafe_exists and psafe_canread and psafe_canwrite and mode != "RW": 188 | log.debug("Opening RO") 189 | self.mode = "RO" 190 | elif not psafe_exists and psafe_canwritebase and mode == "RW": 191 | log.debug("Creating new psafe as RW") 192 | self.mode = "RW" 193 | elif not psafe_exists and psafe_canwrite and mode != "RW": 194 | log.warn("Asked to create a new psafe but mode is set to RO") 195 | raise AccessError, "Asked to create a new safe in RO mode" 196 | elif psafe_exists: 197 | log.warn("Can't read safe %s" % repr(filename)) 198 | raise AccessError, "Can't read %s" % filename 199 | else: 200 | log.warn("Safe doesn't exist or can't read directory") 201 | raise AccessError, "No such safe %s" % filename 202 | if psafe_exists: 203 | self.filename = filename 204 | log.debug("Loading existing safe from %r" % self.filename) 205 | self.fl = open(self.filename, 'rb') 206 | try: 207 | self.flfull = self.fl.read() 208 | log.debug("Full data len: %d" % len(self.flfull)) 209 | self.password = str(password) 210 | # Read in file 211 | self.load() 212 | finally: 213 | self.fl.close() 214 | else: 215 | log.debug("New psafe") 216 | self.password = str(password) 217 | self.filename = filename 218 | # Init local vars 219 | # SALT 220 | self.salt = os.urandom(32) 221 | log.debug("Salt is %s" % repr(self.salt)) 222 | # ITER 223 | self.iter = pow(2, 11) # 2048 224 | log.debug("Iter set to %s" % self.iter) 225 | # K 226 | self.enckey = os.urandom(32) 227 | # L 228 | self.hshkey = os.urandom(32) 229 | # IV 230 | self.iv = os.urandom(16) 231 | # Tag 232 | self.tag = "PWS3" 233 | # EOF 234 | self.eof = "PWS3-EOFPWS3-EOF" 235 | self.headers = [] 236 | self.hmacreq = [] 237 | self.records = [] 238 | # Add EOF headers 239 | self.headers.append(EOFHeader()) 240 | self.autoUpdateHeaders() 241 | 242 | def autoUpdateHeaders(self): 243 | """ Set auto-set headers that should be set on save """ 244 | self.setUUID(updateAutoData = False) 245 | self.setLastSaveApp('pypwsafe', updateAutoData = False) 246 | self.setTimeStampOfLastSave(datetime.datetime.now(), updateAutoData = False) 247 | self.setLastSaveHost(updateAutoData = False) 248 | self.setLastSaveUser(updateAutoData = False) 249 | 250 | def __len__(self): 251 | return len(self.records) 252 | 253 | def save(self): 254 | """ Save the safe to disk 255 | """ 256 | if self.mode == "RW": 257 | self.serialiaze() 258 | fil = open(self.filename, "w") 259 | fil.write(self.flfull) 260 | fil.close() 261 | else: 262 | raise ROSafe, "Safe is not in read/write mode" 263 | 264 | def serialiaze(self): 265 | """ Turn the in-memory objects into in-memory strings. 266 | """ 267 | # P' 268 | self._regen_pprime() 269 | # Regen b1b2 270 | self._regen_b1b2() 271 | # Regen b3b4 272 | self._regen_b3b4() 273 | # Regen H(P') 274 | self._regen_hpprime() 275 | # Regen hmac 276 | self.hmac = self.current_hmac() 277 | 278 | log.debug('Loading psafe') 279 | self.flfull = pack( 280 | '4s32sI32s32s32s16s' 281 | , self.tag 282 | , self.salt 283 | , self.iter 284 | , self.hpprime 285 | , self.b1b2 286 | , self.b3b4 287 | , self.iv 288 | ) 289 | log.debug("Pre-header flfull now %s", (self.flfull,)) 290 | self.fulldata = '' 291 | for header in self.headers: 292 | self.fulldata += header.serialiaze() 293 | # log.debug("In header flfull now %s",(self.flfull,)) 294 | for record in self.records: 295 | self.fulldata += record.serialiaze() 296 | # log.debug("In record flfull now %s",(self.flfull,)) 297 | # Encrypted self.fulldata to self.cryptdata 298 | log.debug("Encrypting header/record data %s" % repr(self.fulldata)) 299 | self.encrypt_data() 300 | self.flfull += self.cryptdata 301 | log.debug("Adding crypt data %s" % repr(self.cryptdata)) 302 | self.flfull += pack('16s32s', self.eof, self.hmac) 303 | log.debug("Post EOF flfull now %s", (self.flfull,)) 304 | 305 | def _regen_pprime(self): 306 | """Regenerate P'. This is the stretched version of salt and password. """ 307 | self.pprime = stretchkey(self.password, self.salt, self.iter) 308 | log.debug("P' = % s" % repr(self.pprime)) 309 | 310 | def _regen_b1b2(self): 311 | """Regenerate b1 and b2. This is the encrypted form of K. 312 | 313 | """ 314 | tw = MCRYPT('twofish', 'ecb') 315 | tw.init(self.pprime) 316 | self.b1b2 = tw.encrypt(self.enckey) 317 | log.debug("B1/B2 set to %s" % repr(self.b1b2)) 318 | 319 | def _regen_b3b4(self): 320 | """Regenerate b3 and b4. This is the encrypted form of L. 321 | """ 322 | tw = MCRYPT('twofish', 'ecb') 323 | tw.init(self.pprime) 324 | self.b3b4 = tw.encrypt(self.hshkey) 325 | log.debug("B3/B4 set to %s" % repr(self.b3b4)) 326 | 327 | def _regen_hpprime(self): 328 | """Regenerate H(P') 329 | Save the SHA256 of self.pprime. 330 | """ 331 | hsh = sha256_func() 332 | hsh.update(self.pprime) 333 | self.hpprime = hsh.digest() 334 | log.debug("Set H(P') to % s" % repr(self.hpprime)) 335 | assert self.check_password() 336 | 337 | def load(self): 338 | """Load a psafe3 file 339 | Will raise PasswordError if the password is bad. 340 | Format: 341 | Name Bytes Type 342 | TAG 4 ASCII 343 | SALT 32 BIN 344 | ITER 4 INT 32 345 | H(P') 32 BIN 346 | B1 16 BIN 347 | B2 16 BIN 348 | B3 16 BIN 349 | B4 16 BIN 350 | IV 16 BIN 351 | Crypted 16n BIN 352 | EOF 16 ASCII 353 | HMAC 32 BIN 354 | """ 355 | log.debug('Loading psafe') 356 | log.debug('len: %d flful: %r' % (len(self.flfull[:152]), self.flfull[:152])) 357 | (self.tag, self.salt, self.iter, self.hpprime, self.b1b2, self.b3b4, self.iv) = unpack('4s32sI32s32s32s16s', self.flfull[:152]) 358 | log.debug("Tag: %s" % repr(self.tag)) 359 | log.debug("Salt: %s" % repr(self.salt)) 360 | log.debug("Iter: %s" % repr(self.iter)) 361 | log.debug("H(P'): % s" % repr(self.hpprime)) 362 | log.debug("B1B2: % s" % repr(self.b1b2)) 363 | log.debug("B3B4: % s" % repr(self.b3b4)) 364 | log.debug("IV: % s" % repr(self.iv)) 365 | self.cryptdata = self.flfull[152:-48] 366 | (self.eof, self.hmac) = unpack('16s32s', self.flfull[-48:]) 367 | log.debug("EOF: % s" % repr(self.eof)) 368 | log.debug("HMAC: % s" % repr(self.hmac)) 369 | # Determine the password hash 370 | self.update_pprime() 371 | # Verify password 372 | if not self.check_password(): 373 | raise PasswordError 374 | # Figure out the encryption and hash session keys 375 | log.debug("Calc'ing keys") 376 | self.calc_keys() 377 | log.debug("Going to decrypt data") 378 | self.decrypt_data() 379 | 380 | # Parse headers 381 | self.headers = [] 382 | self.hmacreq = [] 383 | self.remaining_headers = self.fulldata 384 | hdr = Create_Header(self._fetch_block) 385 | self.headers.append(hdr) 386 | self.hmacreq.append(hdr.hmac_data) 387 | # print str(hdr) +" - -"+ repr(hdr) 388 | while type(hdr) != EOFHeader: 389 | hdr = Create_Header(self._fetch_block) 390 | self.headers.append(hdr) 391 | # print str(hdr) +" - -"+ repr(hdr) 392 | 393 | # Parse DB 394 | self.records = [] 395 | while len(self.remaining_headers) > 0: 396 | req = Record(self._fetch_block) 397 | self.records.append(req) 398 | 399 | if self.current_hmac(cached = True) != self.hmac: 400 | log.error('Invalid HMAC Calculated: %s File: %s' % (repr(self.current_hmac()), repr(self.hmac))) 401 | raise InvalidHMACError, "Calculated: % s File: % s" % (repr(self.current_hmac()), repr(self.hmac)) 402 | 403 | def __str__(self): 404 | ret = '' 405 | for i in self.records: 406 | ret += str(i) + "\n\n" 407 | return ret 408 | 409 | def _fetch_block(self, num_blocks = 1): 410 | """Returns one or more 16 - byte block of data. Raises EOFError when there is no more data. """ 411 | assert num_blocks > 0 412 | bytes = num_blocks * 16 413 | if bytes > len(self.remaining_headers): 414 | raise EOFError, "No more header data" 415 | ret = self.remaining_headers[:bytes] 416 | self.remaining_headers = self.remaining_headers[bytes:] 417 | return ret 418 | 419 | def calc_keys(self): 420 | """Calculate sessions keys for encryption and hmac. Is based on pprime, b1b2, b3b4""" 421 | tw = MCRYPT('twofish', 'ecb') 422 | tw.init(self.pprime) 423 | self.enckey = tw.decrypt(self.b1b2) 424 | # its ok to reuse; ecb doesn't keep state info 425 | self.hshkey = tw.decrypt(self.b3b4) 426 | log.debug("Encryption key K: %s " % repr(self.enckey)) 427 | log.debug("HMAC Key L: %s " % repr(self.hshkey)) 428 | 429 | def decrypt_data(self): 430 | """Decrypt encrypted portion of header and data""" 431 | log.debug("Creating mcrypt object") 432 | tw = MCRYPT('twofish', 'cbc') 433 | log.debug("Adding key & iv") 434 | tw.init(self.enckey, self.iv) 435 | log.debug("Decrypting data") 436 | self.fulldata = tw.decrypt(self.cryptdata) 437 | 438 | def encrypt_data(self): 439 | """Encrypted fulldata to cryptdata""" 440 | tw = MCRYPT('twofish', 'cbc') 441 | tw.init(self.enckey, self.iv) 442 | self.cryptdata = tw.encrypt(self.fulldata) 443 | 444 | def current_hmac(self, cached = False): 445 | """Returns the current hmac of self.fulldata""" 446 | data = '' 447 | for i in self.headers: 448 | log.debug("Adding hmac data %r from %r" % (i.hmac_data(), i.__class__.__name__)) 449 | if cached: 450 | data += i.data 451 | else: 452 | data += i.hmac_data() 453 | # assert i.data == i.hmac_data(), "Working on %r where %r!=%r" % (i, i.data, i.hmac_data()) 454 | for i in self.records: 455 | # TODO: Add caching support 456 | log.debug("Adding hmac data %r from %r" % (i.hmac_data(), i.__class__.__name__)) 457 | data += i.hmac_data() 458 | log.debug("Building hmac with key %s", repr(self.hshkey)) 459 | hm = HMAC(self.hshkey, data, sha256_mod) 460 | # print hm.hexdigest() 461 | log.debug("HMAC %s-%s", repr(hm.hexdigest()), repr(hm.digest())) 462 | return hm.digest() 463 | 464 | def check_password(self): 465 | """Check that the hash in self.pprime matches what's in the password safe. True if password matches hash in hpprime. False otherwise""" 466 | hsh = sha256_func() 467 | hsh.update(self.pprime) 468 | return hsh.digest() == self.hpprime 469 | 470 | def update_pprime(self): 471 | """Update self.pprime. This key is used to decrypt B1 / 2 and B3 / 4""" 472 | self.pprime = stretchkey(self.password, self.salt, self.iter) 473 | 474 | def close(self): 475 | """Close out open file""" 476 | self.fl.close() 477 | 478 | def __del__(self): 479 | try: 480 | self.fl.close() 481 | except: 482 | pass 483 | 484 | def listall(self): 485 | """ 486 | Yield all entries in the form 487 | (uuid, title, group, username, password, notes) 488 | @rtype: [(uuid, title, group, username, password, notes),...] 489 | @return A list of tuples covering all known records. 490 | """ 491 | def nrwrapper(name): 492 | try: 493 | return record[name] 494 | except KeyError: 495 | return None 496 | 497 | for record in self.records: 498 | yield ( 499 | nrwrapper('UUID') 500 | , nrwrapper('Title') 501 | , nrwrapper('Group') 502 | , nrwrapper('Username') 503 | , nrwrapper('Password') 504 | , nrwrapper('Notes') 505 | ) 506 | 507 | def getEntries(self): 508 | """ Return a list of all records 509 | @rtype: [Record,...] 510 | @return: A list of all records. 511 | """ 512 | return self.records 513 | 514 | def getpass(self, uuid = None): 515 | """Returns the password of the item with the given UUID 516 | @param uuid: UUID of the record to find 517 | @type uuid: UUID object 518 | @rtype: string 519 | @return: Password for the record with the given UUID. Raise an exception otherwise 520 | @raise UUIDNotFoundError 521 | """ 522 | for record in self.records: 523 | if record['UUID'] == uuid: 524 | return record['Password'] 525 | raise UUIDNotFoundError, "UUID %s was not found. " % repr(uuid) 526 | 527 | def __getitem__(self, *args, **kwargs): 528 | return self.records.__getitem__(*args, **kwargs) 529 | 530 | def __setitem__(self, *args, **kwargs): 531 | return self.records.__setitem__(*args, **kwargs) 532 | 533 | def getUUID(self): 534 | """Return the safe's uuid""" 535 | return _getHeaderField(self.headers, UUIDHeader) 536 | 537 | def setUUID(self, uuid = None, updateAutoData = True): 538 | if updateAutoData: 539 | self.autoUpdateHeaders() 540 | 541 | if uuid is None: 542 | uuid = uuid4() 543 | 544 | if not _setHeaderField(self.headers, UUIDHeader, uuid): 545 | self.headers.insert(0, UUIDHeader(uuid = uuid)) 546 | 547 | def removeUUID(self): 548 | _setHeaderField(self.headers, UUIDHeader, None) 549 | 550 | def getVersion(self): 551 | """Return the safe's version""" 552 | return _getHeaderField(self.headers, VersionHeader, 'version') 553 | 554 | def setVersion(self, version = None, updateAutoData = True): 555 | """Return the safe's version""" 556 | if updateAutoData: 557 | self.autoUpdateHeaders() 558 | 559 | if not _setHeaderField(self.headers, VersionHeader, version): 560 | self.headers.insert(0, VersionHeader(version = version)) 561 | 562 | def getVersionPretty(self): 563 | """Return the safe's version""" 564 | hdr = _findHeader(self.headers, VersionHeader) 565 | if hdr: 566 | return hdr.getVersionHuman() 567 | return None 568 | 569 | def setVersionPretty(self, version = None, updateAutoData = True): 570 | """Return the safe's version""" 571 | if updateAutoData: 572 | self.autoUpdateHeaders() 573 | 574 | hdr = _findHeader(self.headers, VersionHeader) 575 | if hdr: 576 | hdr.setVersionHuman(version) 577 | else: 578 | n = VersionHeader(version = 0x00) 579 | n.setVersionHuman(version = version) 580 | self.headers.insert(0, n) 581 | 582 | def getTimeStampOfLastSave(self): 583 | return _getHeaderField(self.headers, TimeStampOfLastSaveHeader, 'lastsave') 584 | 585 | def setTimeStampOfLastSave(self, timestamp, updateAutoData = True): 586 | if updateAutoData: 587 | self.autoUpdateHeaders() 588 | 589 | if not _setHeaderField(self.headers, TimeStampOfLastSaveHeader, timestamp.timetuple()): 590 | self.headers.insert(0, TimeStampOfLastSaveHeader(lastsave = timestamp.timetuple())) 591 | 592 | def getLastSaveApp(self): 593 | return _getHeaderField(self.headers, LastSaveAppHeader, 'lastSafeApp') 594 | 595 | def setLastSaveApp(self, app, updateAutoData = True): 596 | if updateAutoData: 597 | self.autoUpdateHeaders() 598 | 599 | if not _setHeaderField(self.headers, LastSaveAppHeader, app): 600 | self.headers.insert(0, LastSaveAppHeader(lastSaveApp = app)) 601 | 602 | def getLastSaveUser(self, fallbackOld = True): 603 | ret = self.getLastSaveUserNew() 604 | if ret or not fallbackOld: 605 | return ret 606 | return self.getLastSaveUserOld() 607 | 608 | def getLastSaveUserNew(self): 609 | """ Get the last saving user using only the non-deprecated field """ 610 | return _getHeaderField(self.headers, LastSaveUserHeader, 'username') 611 | 612 | def getLastSaveUserOld(self): 613 | """ Get the last saving user using only the deprecated 0x05 field """ 614 | return _getHeaderField(self.headers, WhoLastSavedHeader) 615 | 616 | def setLastSaveUser(self, username = None, updateAutoData = True, addOld = False): 617 | if updateAutoData: 618 | self.autoUpdateHeaders() 619 | if username is None: 620 | username = getpass.getuser() 621 | 622 | if not _setHeaderField(self.headers, LastSaveUserHeader, username): 623 | self.headers.insert(0, LastSaveUserHeader(username = username)) 624 | if addOld and not _setHeaderField(self.headers, WhoLastSavedHeader, username): 625 | self.headers.insert(0, WhoLastSavedHeader(username = username)) 626 | 627 | def getLastSaveHost(self): 628 | return _getHeaderField(self.headers, LastSaveHostHeader, 'hostname') 629 | 630 | def setLastSaveHost(self, hostname = None, updateAutoData = True): 631 | if updateAutoData: 632 | self.autoUpdateHeaders() 633 | if not hostname: 634 | hostname = socket.gethostname() 635 | 636 | if not _setHeaderField(self.headers, LastSaveHostHeader, hostname): 637 | self.headers.insert(0, LastSaveHostHeader(hostname = hostname)) 638 | 639 | def getDbName(self): 640 | """ Returns the name of the db according to the psafe headers """ 641 | return _getHeaderField(self.headers, DBNameHeader, 'dbName') 642 | 643 | def setDbName(self, dbName, updateAutoData = True): 644 | """ Returns the name of the db according to the psafe headers """ 645 | if updateAutoData: 646 | self.autoUpdateHeaders() 647 | 648 | if not _setHeaderField(self.headers, DBNameHeader, dbName): 649 | self.headers.insert(0, DBNameHeader(dbName = dbName)) 650 | 651 | def getDbDesc(self): 652 | """ Returns the description of the db according to the psafe headers """ 653 | return _getHeaderField(self.headers, DBDescHeader, 'dbDesc') 654 | 655 | def setDbDesc(self, dbDesc, updateAutoData = True): 656 | """ Returns the description of the db according to the psafe headers """ 657 | if updateAutoData: 658 | self.autoUpdateHeaders() 659 | 660 | if not _setHeaderField(self.headers, DBDescHeader, dbDesc): 661 | self.headers.insert(0, DBDescHeader(dbDesc = dbDesc)) 662 | 663 | def getDbPolicies(self): 664 | """ Return a list of all named password policies """ 665 | return _getHeaderField(self.headers, NamedPasswordPoliciesHeader) 666 | 667 | def setDbPolicies(self, dbName, updateAutoData = True): 668 | """ Returns the name of the db according to the psafe headers """ 669 | raise NotImplementedError("FIXME: Add db policy control methods") 670 | 671 | def getDbRecentEntries(self): 672 | """ Return a list of recent headers """ 673 | return _getHeaderFields(self.headers, RecentEntriesHeader) 674 | 675 | def setDbRecentEntries(self, entryUUID, updateAutoData = True): 676 | """ Returns the name of the db according to the psafe headers """ 677 | raise NotImplementedError("FIXME: Add db recent entries control methods") 678 | 679 | def getDbPrefs(self): 680 | """ Return a list of recent headers """ 681 | return _getHeaderField(self.headers, NonDefaultPrefsHeader) 682 | 683 | def setDbPrefs(self, prefs, updateAutoData = True): 684 | """ Returns the name of the db according to the psafe headers """ 685 | if updateAutoData: 686 | self.autoUpdateHeaders() 687 | 688 | if not _setHeaderField(self.headers, NonDefaultPrefsHeader, prefs): 689 | self.headers.insert(0, NonDefaultPrefsHeader(**prefs)) 690 | 691 | def setDbPref(self, prefName, prefValue, updateAutoData = True): 692 | """ Returns the name of the db according to the psafe headers """ 693 | if updateAutoData: 694 | self.autoUpdateHeaders() 695 | 696 | hdr = _findHeader(self.headers, NonDefaultPrefsHeader) 697 | if hdr: 698 | attr = getattr(hdr, NonDefaultPrefsHeader.FIELD) 699 | attr[prefName] = prefValue 700 | else: 701 | self.headers.insert(0, NonDefaultPrefsHeader(prefName = prefValue)) 702 | 703 | def getEmptyGroups(self): 704 | """ Return a list of empty group names """ 705 | return _getHeaderFields(self.headers, EmptyGroupHeader) 706 | 707 | def setEmptyGroups(self, groups, updateAutoData = True): 708 | """ Removes all existing empty group headers and adds one as given by groups """ 709 | if updateAutoData: 710 | self.autoUpdateHeaders() 711 | 712 | for hdr in self.headers: 713 | if type(hdr) == EmptyGroupHeader: 714 | self.headers.remove(hdr) 715 | 716 | for groupName in groups: 717 | self.headers.insert(0, EmptyGroupHeader(groupName = groupName)) 718 | 719 | def addEmptyGroup(self, groupName, updateAutoData = True): 720 | """ Removes all existing empty group headers and adds one as given by groups """ 721 | if updateAutoData: 722 | self.autoUpdateHeaders() 723 | 724 | assert groupName not in self.getEmptyGroups() 725 | 726 | self.headers.insert(0, EmptyGroupHeader(groupName = groupName)) 727 | 728 | def _get_lock_data(self): 729 | """ Returns a string representing the data that should be stored in the lockfile 730 | For details about Password Safe's implementation see: http://passwordsafe.git.sourceforge.net/git/gitweb.cgi?p=passwordsafe/pwsafe.git;a=blob;f=pwsafe/pwsafe/src/os/windows/file.cpp 731 | """ 732 | pid = os.getpid() 733 | username = getpass.getuser() 734 | host = socket.gethostname() 735 | return "%s@%s:%d" % (username, host, pid) 736 | 737 | # Example Lockfile data: 'myusername@myhostname:12345' 738 | LOCKFILE_PARSE_RE = re.compile(r'^(.*)@([^@:]*):(\d+)$') 739 | 740 | def lock(self): 741 | """ Acquire a lock on the DB. Raise an exception on failure. Raises an error 742 | if the lock has already be acquired by this process or another. 743 | Note: Make sure to wrap the post-lock, pre-unlock in a try-finally 744 | so that the safe is always unlocked. 745 | Note: The type of locking/unlocking used should be compatable with 746 | the actuall Password Safe app. If the psafe save dir is shared via 747 | NFS/CIFS/etc then users of the share should be able to read/write/lock/unlock 748 | psafe files. 749 | Note: No gurentee that this will work in Windows 750 | """ 751 | 752 | # Use splitext() to handle the case where the file may not have psafe3 ext or any extension at all. 753 | # Note the full path of filename is not lost when the extension is split off. 754 | filename, _ = os.path.splitext(self.filename) 755 | lfile = os.path.extsep.join((filename, 'plk')) 756 | 757 | log.debug("Going to lock %r using %r", self, lfile) 758 | 759 | # Make sure we don't already hold the lock 760 | if self.locked and os.access(lfile, os.R_OK): 761 | raise LockAlreadyAcquiredError 762 | 763 | if os.path.isfile(lfile): 764 | # May be a dead pid 765 | log.debug("Lock file already exists. Reading it. ") 766 | f = open(lfile, 'rb') 767 | data = f.read() 768 | f.close() 769 | found = self.LOCKFILE_PARSE_RE.findall(data) 770 | log.debug("Got %r from the lock", found) 771 | if len(found) == 1: 772 | (lusername, lhostname, lpid) = found[0] 773 | if lhostname == socket.gethostbyname(): 774 | try: # Check if the other proc is still alive 775 | os.kill(pid, 0) # @UndefinedVariable 776 | log.info("Other process (PID: %r) is alive. Can't override lock for %r ", lpid, self) 777 | raise AlreadyLockedError, "Other process is alive. Can't override lock. " 778 | except: 779 | # Not really locked, remove stale lock 780 | log.warning("Removing stale lock file of %r at %r", self, lfile) 781 | os.remove(lfile) 782 | return self.lock() 783 | else: 784 | log.info("Lock file is for a different host (%r). Assuming %r is locked. ", lhostname, self) 785 | raise AlreadyLockedError, "Lock is on a different host. Can't try to unlock. " 786 | else: 787 | log.info("Lock file contains invalid data: %r Assuming the safe, %r, is already locked. ", found, self) 788 | raise AlreadyLockedError, "Lock file contains invalid data. Assuming the safe is already locked. " 789 | self.locked = lfile 790 | 791 | # Create the lock file with no race conditions 792 | # Should generate an OS error if the file already exists 793 | try: 794 | fd = os.open(lfile, os.O_CREAT | os.O_EXCL | os.O_RDWR) 795 | os.write(fd, self._get_lock_data()) 796 | os.close(fd) 797 | except OSError, e: 798 | log.info("%r reported as unlocked but can't create the lockfile", self) 799 | raise AlreadyLockedError 800 | 801 | def unlock(self): 802 | """ Unlock the DB 803 | Note: See lock method for important locking info. 804 | """ 805 | if not self.locked: 806 | log.info("%r is not locked. Failing to unlock. ", self) 807 | raise NotLockedError, "Not currently locked" 808 | try: 809 | os.remove(self.locked) 810 | self.locked = False 811 | log.debug("%r for %r is unlocked", self.locked, self) 812 | except OSError: 813 | log.info("%r reported as locked but no lock file exists", self) 814 | raise NotLockedError, "Obj reported as locked but no lock file exists" 815 | 816 | def forceUnlock(self): 817 | """ Try to unlock and remove the lock file by force. 818 | Note: File permissions can cause this to fail. 819 | @return: True if a lock file was removed. False otherwise. 820 | """ 821 | lfile = self.filename.replace('.psafe3', '.plk') 822 | log.debug("Going to remove lock file %r from %r by force. Local obj lock status: %r", lfile, self, self.locked) 823 | self.locked = False 824 | try: 825 | os.remove(lfile) 826 | log.info("Removed lock file %r by force", lfile) 827 | return True 828 | except OSError: 829 | log.debug("Lock file %r doesn't exist", lfile) 830 | return False 831 | 832 | # Misc helper functions 833 | def ispsafe3(filename): 834 | """Return True if the file appears to be a psafe v3 file. Does not do in-depth checks. """ 835 | fil = open(filename, "r") 836 | data = fil.read(4) 837 | fil.close() 838 | return data == "PWS3" 839 | 840 | if __name__ == "__main__": 841 | import doctest 842 | doctest.testmod() 843 | -------------------------------------------------------------------------------- /src/pypwsafe/PWSafeV3Headers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #=============================================================================== 3 | # SYMANTEC: Copyright (C) 2009-2011 Symantec Corporation. All rights reserved. 4 | # 5 | # This file is part of PyPWSafe. 6 | # 7 | # PyPWSafe is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # PyPWSafe is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with PyPWSafe. If not, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html 19 | #=============================================================================== 20 | """ Header objects for psafe v3 21 | 22 | @author: Paulson McIntyre 23 | @license: GPLv2 24 | @version: 0.1 25 | """ 26 | # Note: Use "=" in all packs to account for 64bit systems 27 | 28 | from struct import unpack, pack 29 | from errors import * 30 | from consts import * 31 | import os 32 | import logging, logging.config 33 | from uuid import UUID, uuid4 34 | from pprint import pformat 35 | from binascii import unhexlify 36 | 37 | # logging.config.fileConfig('/etc/mss/psafe_log.conf') 38 | log = logging.getLogger("psafe.lib.header") 39 | log.debug('initing') 40 | 41 | headers = { } 42 | 43 | class _HeaderType(type): 44 | def __init__(cls, name, bases, dct): 45 | super(_HeaderType, cls).__init__(name, bases, dct) 46 | # Skip any where TYPE is none, such as the base class 47 | if cls.TYPE: 48 | # Make sure no type ids are duplicated 49 | assert not headers.has_key(cls.TYPE) 50 | headers[cls.TYPE] = cls 51 | 52 | class Header(object): 53 | """A psafe3 header object. Should be extended. This also servers as a "unknown" header type. 54 | raw_data string Real data that was passed 55 | data string Raw data minus padding and headers 56 | len long Number of bytes of data. May not be present until data has been parsed 57 | readblock_f function Read in another block of data 58 | TYPE int Header type that IDs it in psafe3 59 | 60 | """ 61 | # Auto-register new classes 62 | __metaclass__ = _HeaderType 63 | 64 | TYPE = None 65 | FIELD = None 66 | 67 | def __init__(self, htype, hlen, raw_data): 68 | self.data = raw_data[5:(hlen + 5)] 69 | self.raw_data = raw_data 70 | self.len = int(hlen) 71 | if type(self) != Header: 72 | assert self.TYPE == htype 73 | else: 74 | self.TYPE = htype 75 | self.parse() 76 | 77 | def parse(self): 78 | """Parse the header. Should be overridden. """ 79 | pass 80 | 81 | def gen_blocks(self): 82 | """Returns the raw data that should be stuck in a psafe file""" 83 | return self.raw_data 84 | 85 | def __repr__(self): 86 | # Can no longer depend on raw_data existing 87 | # return "Header(%s,%d,%s)"%(repr(self.TYPE),self.len,repr(self.raw_data)) 88 | s = self.serial() 89 | return "Header(%s,%d,%s)" % (repr(self.TYPE), len(s), repr(s)) 90 | 91 | def __str__(self): 92 | return self.__repr__() 93 | 94 | def hmac_data(self): 95 | """Returns the data segments that should be used for the HMAC. See bug 1812081. """ 96 | return self.serial() 97 | 98 | def serial(self): 99 | return self.data 100 | 101 | def serialiaze(self): 102 | serial = self.serial() 103 | log.debug("len: %s type: %s final: %s" % (len(serial), repr(chr(self.TYPE)), repr(pack('=lc', len(serial), chr(self.TYPE))))) 104 | padded = self._pad(pack('=lc', len(serial), chr(self.TYPE)) + serial) 105 | log.debug("Padded data %s" % repr(padded)) 106 | return padded 107 | 108 | def _pad(self, data): 109 | """ Pad out data to 16 bytes """ 110 | add_data = 16 - len(data) % 16 111 | if add_data == 16: 112 | add_data = 0 113 | padding = '' 114 | for i in xrange(0, add_data): 115 | padding += os.urandom(1) 116 | assert len(padding) == add_data 117 | assert len(data + padding) % 16 == 0 118 | return data + padding 119 | 120 | 121 | class VersionHeader(Header): 122 | """Version header object 123 | version int Psafe version 124 | 125 | >>> x=VersionHeader(0,2,'\x02\x00\x00\x00\x00\x02\x03\xb45C\x1d\xea\x08\x155\x02') 126 | >>> str(x) 127 | 'Version=0x302' 128 | >>> repr(x) 129 | "VersionHeader(0,2,'\\x02\\x00\\x00\\x00\\x00\\x02\\x03\\xb45C\\x1d\\xea\\x08\\x155\\x02')" 130 | >>> x.serial() 131 | '\x02\x03' 132 | >>> x=VersionHeader(version=0x304) 133 | >>> str(x) 134 | 'Version=0x304' 135 | >>> x.serial() 136 | '\x04\x03' 137 | """ 138 | TYPE = 0x00 139 | FIELD = 'version' 140 | 141 | def __init__(self, htype = None, hlen = 2, raw_data = None, version = 0x305): 142 | if not htype: 143 | htype = self.TYPE 144 | if raw_data: 145 | Header.__init__(self, htype, hlen, raw_data) 146 | else: 147 | self.version = version 148 | 149 | def parse(self): 150 | """Parse data""" 151 | self.version = unpack('=H', self.data)[0] 152 | 153 | def getVersionHuman(self): 154 | if self.version in version_map: 155 | return version_map[self.version] 156 | return "Unknown Version %r" % self.version 157 | 158 | def setVersionHuman(self, version): 159 | if version in version_map: 160 | self.version = version_map[version] 161 | else: 162 | raise ValueError("Unknown version name %r" % version) 163 | 164 | def __repr__(self): 165 | return "Version" + Header.__repr__(self) 166 | 167 | def __str__(self): 168 | return "Version=%s" % hex(self.version) 169 | 170 | def serial(self): 171 | return pack('=H', self.version) 172 | 173 | class UUIDHeader(Header): 174 | """DB UUID 175 | uuid uuid.UUID Database uuid object 176 | 177 | DHeader(1,16,'\x10\x00\x00\x00\x01\xbdV\x92{H\xdbL\xec\xbb+\xe90w5\x17\xa2P6b\xe8\x87\x0c\x83\n\xd8\x11\xd7') 178 | >>> x.serial() 179 | '\xbdV\x92{H\xdbL\xec\xbb+\xe90w5\x17\xa2' 180 | >>> str(x) 181 | "UUID=UUID('bd56927b-48db-4cec-bb2b-e930773517a2')" 182 | >>> repr(x) 183 | "UUIDHeader(1,16,'\\x10\\x00\\x00\\x00\\x01\\xbdV\\x92{H\\xdbL\\xec\\xbb+\\xe90w5\\x17\\xa2P6b\\xe8\\x87\\x0c\\x83\\n\\xd8\\x11\\xd7')" 184 | """ 185 | TYPE = 0x01 186 | FIELD = 'uuid' 187 | 188 | def __init__(self, htype = None, hlen = 16, raw_data = None, uuid = None): 189 | if not htype: 190 | htype = self.TYPE 191 | if raw_data: 192 | Header.__init__(self, htype, hlen, raw_data) 193 | else: 194 | if uuid: 195 | self.uuid = uuid 196 | else: 197 | self.uuid = uuid4() 198 | 199 | def parse(self): 200 | """Parse data""" 201 | self.uuid = UUID(bytes = unpack('=16s', self.data)[0]) 202 | 203 | def __repr__(self): 204 | return "UUID" + Header.__repr__(self) 205 | 206 | def __str__(self): 207 | return "UUID=%s" % repr(self.uuid) 208 | 209 | def serial(self): 210 | return pack('=16s', str(self.uuid.bytes)) 211 | 212 | class NonDefaultPrefsHeader(Header): 213 | """Version header object 214 | version int Psafe version 215 | opts dict All config options 216 | 217 | K:V for opts: 218 | 219 | 220 | >>> x=NonDefaultPrefsHeader(2,70,'B 1 1 B 2 1 B 28 1 B 29 1 B 31 1 B 50 0 I 12 255 I 17 1 I 18 1 I 20 1 ') 221 | >>> x=NonDefaultPrefsHeader(2,86,'B 1 1 B 2 1 B 28 1 B 29 1 B 31 1 B 50 0 I 12 255 I 17 1 I 18 1 I 20 1 S 3 \'adfasdfs"\' ') 222 | # FIXME: Fill in tests 223 | """ 224 | TYPE = 0x02 225 | FIELD = 'opts' 226 | 227 | def __init__(self, htype = None, hlen = 2, raw_data = None, **kw): 228 | if not htype: 229 | htype = self.TYPE 230 | if raw_data: 231 | Header.__init__(self, htype, hlen, raw_data) 232 | else: 233 | self.opts = kw 234 | 235 | def parse(self): 236 | """Parse data""" 237 | self.opts = {} 238 | remander = self.data.split(' ') 239 | while len(remander) > 2: 240 | # Pull out the data 241 | rtype = str(remander[0]) 242 | key = int(remander[1]) 243 | value = str(remander[2]) 244 | del remander[0:3] 245 | if rtype == "B": 246 | found = False 247 | for name, info in conf_bools.items(): 248 | if info['index'] == key: 249 | found = True 250 | break 251 | if not found: 252 | raise ConfigItemNotFoundError, "%d is not a valid configuration item" % key 253 | if value == "0": 254 | self.opts[name] = False 255 | elif value == "1": 256 | self.opts[name] = True 257 | else: 258 | raise PrefsValueError, "Expected either 0 or 1 for bool type, got %r" % value 259 | elif rtype == "I": 260 | found = False 261 | for name, info in conf_ints.items(): 262 | if info['index'] == key: 263 | found = True 264 | break 265 | if not found: 266 | raise ConfigItemNotFoundError, "%d is not a valid configuration item" % key 267 | try: 268 | value = int(value) 269 | except ValueError: 270 | raise PrefsDataTypeError, "%r is not a valid int" % value 271 | if info['min'] != -1 and info['min'] > value: 272 | raise PrefsDataTypeError, "%r is too small" % value 273 | if info['max'] != -1 and info['max'] < value: 274 | raise PrefsDataTypeError, "%r is too big" % value 275 | self.opts[name] = value 276 | elif rtype == "S": 277 | found = False 278 | for name, info in conf_strs.items(): 279 | if info['index'] == key: 280 | found = True 281 | break 282 | if not found: 283 | raise ConfigItemNotFoundError, "%d is not a valid configuration item" % key 284 | # Remove "" or whatever the delimiter is 285 | delm = value[0] 286 | if value[-1] == delm: 287 | value = value[1:-1] 288 | else: 289 | while not delm in remander[0] and len(remander) > 0: 290 | value += remander[0] 291 | del remander[0] 292 | value = value[1:-1] 293 | # Save the pref 294 | self.opts[name] = value 295 | else: 296 | raise PrefsDataTypeError, "Unexpected record type for preferences %r" % rtype 297 | # Fill in defaults prefs 298 | for typeS in [conf_bools, conf_ints, conf_strs]: 299 | for name, info in typeS.items(): 300 | if name not in self.opts and info['type'] == ptDatabase: 301 | self.opts[name] = info['default'] 302 | 303 | def __repr__(self): 304 | return "NonDefaultPrefs" + Header.__repr__(self) 305 | 306 | def __str__(self): 307 | return "NonDefaultPrefs=%s" % pformat(self.opts) 308 | 309 | def serial(self): 310 | ret = '' 311 | for name, value in self.opts.items(): 312 | if not conf_types.has_key(name): 313 | raise PrefsValueError, "%r is not a valid configuration option" % name 314 | typ = conf_types[name] 315 | if type(value) != typ: 316 | raise PrefsDataTypeError, "%r is not a valid type for the key %r" % (type(value), name) 317 | if typ == bool: 318 | if value == conf_bools[name]['default']: 319 | # Default value - Don't save 320 | continue 321 | if value is True: 322 | value = 1 323 | elif value is False: 324 | value = 0 325 | else: 326 | raise PrefsDataTypeError, "%r is not a valid value for the key %r" % (value, name) 327 | ret += "B %d %d " % (conf_bools[name]['index'], value) 328 | elif typ == int: 329 | value = int(value) 330 | if value == conf_ints[name]['default']: 331 | # Default value - Don't save 332 | continue 333 | ret += "I %d %d " % (conf_ints[name]['index'], value) 334 | elif typ == str: 335 | value = str(value) 336 | if value == conf_strs[name]['default']: 337 | # Default value - Don't save 338 | continue 339 | delms = list("\"'#?!%&*+=:;@~<>?,.{}[]()\xbb") 340 | delm = None 341 | while delm is None and len(delms) > 0: 342 | if not delms[0] in value: 343 | delm = delms[0] 344 | else: 345 | del delms[0] 346 | if not delm: 347 | raise UnableToFindADelimitersError, "Couldn't find a delminator for %r" % value 348 | ret += "S %d %s%s%s " % (conf_strs[name]['index'], delm, value, delm) 349 | else: 350 | raise PrefsDataTypeError, "Unexpected record type for preferences %r" % typ 351 | return ret 352 | 353 | # Header(3,14,'00000000000000'), 354 | class TreeDisplayStatusHeader(Header): 355 | """ Tree display status (what folders are expanded/collapsed 356 | 357 | """ 358 | TYPE = 0x03 359 | FIELD = 'status' 360 | 361 | def __init__(self, htype = None, hlen = 1, raw_data = None, status = ''): 362 | if not htype: 363 | htype = self.TYPE 364 | if raw_data: 365 | Header.__init__(self, htype, hlen, raw_data) 366 | else: 367 | self.status = status 368 | 369 | def parse(self): 370 | """Parse data""" 371 | self.status = self.data 372 | 373 | def __repr__(self): 374 | return "Status" + Header.__repr__(self) 375 | 376 | def __str__(self): 377 | return "Status=%r" % self.status 378 | 379 | def serial(self): 380 | return self.status 381 | 382 | # Header(4,4,'Ao\xc8L'), 383 | from pypwsafe.PWSafeV3Records import parsedatetime, makedatetime 384 | import time 385 | class TimeStampOfLastSaveHeader(Header): 386 | """ Timestamp of last save. 387 | lastsave time struct Last save time of DB 388 | """ 389 | TYPE = 0x04 390 | FIELD = 'lastsave' 391 | 392 | def __init__(self, htype = None, hlen = 1, raw_data = None, lastsave = time.gmtime()): 393 | if not htype: 394 | htype = self.TYPE 395 | if raw_data: 396 | Header.__init__(self, htype, hlen, raw_data) 397 | else: 398 | self.lastsave = lastsave 399 | 400 | def parse(self): 401 | """Parse data""" 402 | time_data = self.data 403 | if len(time_data) == 8: 404 | time_data = unhexlify(time_data) 405 | self.lastsave = time.gmtime(unpack('=i', time_data)[0]) 406 | 407 | def __repr__(self): 408 | return "LastSave" + Header.__repr__(self) 409 | 410 | def __str__(self): 411 | return "LastSave(%r)" % time.strftime("%a, %d %b %Y %H:%M:%S +0000", self.lastsave) 412 | 413 | def serial(self): 414 | return makedatetime(self.lastsave) 415 | 416 | 417 | class WhoLastSavedHeader(Header): 418 | """ User who last saved the DB. *DEPRECATED* 419 | """ 420 | TYPE = 0x05 421 | FIELD = 'username' 422 | 423 | def __init__(self, htype = None, hlen = 1, raw_data = None, username = ''): 424 | if not htype: 425 | htype = self.TYPE 426 | if raw_data: 427 | Header.__init__(self, htype, hlen, raw_data) 428 | else: 429 | self.username = username 430 | 431 | def parse(self): 432 | """Parse data""" 433 | self.username = self.data 434 | 435 | def __repr__(self): 436 | return "LastSave" + Header.__repr__(self) 437 | 438 | def __str__(self): 439 | return "LastSaveUser(%r)" % self.username 440 | 441 | def serial(self): 442 | return self.username 443 | 444 | 445 | # Header(6,19,'Password Safe V3.23'), 446 | class LastSaveAppHeader(Header): 447 | """ What app performed the last save 448 | lastSaveApp string Last saved by this app 449 | """ 450 | TYPE = 0x06 451 | FIELD = 'lastSaveApp' 452 | 453 | def __init__(self, htype = None, hlen = 1, raw_data = None, lastSaveApp = ''): 454 | if not htype: 455 | htype = self.TYPE 456 | if raw_data: 457 | Header.__init__(self, htype, hlen, raw_data) 458 | else: 459 | self.lastSaveApp = lastSaveApp 460 | 461 | def parse(self): 462 | """Parse data""" 463 | self.lastSaveApp = self.data 464 | 465 | def __repr__(self): 466 | return "LastSaveApp" + Header.__repr__(self) 467 | 468 | def __str__(self): 469 | return "LastSaveAppHeader=%r" % self.lastSaveApp 470 | 471 | def serial(self): 472 | return self.lastSaveApp 473 | 474 | # Header(7,6,'owenst'), 475 | class LastSaveUserHeader(Header): 476 | """ User who last saved the DB. 477 | username string 478 | """ 479 | TYPE = 0x07 480 | FIELD = 'username' 481 | 482 | def __init__(self, htype = None, hlen = 1, raw_data = None, username = ''): 483 | if not htype: 484 | htype = self.TYPE 485 | if raw_data: 486 | Header.__init__(self, htype, hlen, raw_data) 487 | else: 488 | self.username = username 489 | 490 | def parse(self): 491 | """Parse data""" 492 | self.username = self.data 493 | 494 | def __repr__(self): 495 | return "LastSaveUser" + Header.__repr__(self) 496 | 497 | def __str__(self): 498 | return "LastSaveUserHeader(%r)" % self.username 499 | 500 | def serial(self): 501 | return self.username 502 | 503 | # Header(8,15,'SOMEHOSTNAME'), 504 | class LastSaveHostHeader(Header): 505 | """ Host that last saved the DB 506 | hostname string 507 | """ 508 | TYPE = 0x08 509 | FIELD = 'hostname' 510 | 511 | def __init__(self, htype = None, hlen = 1, raw_data = None, hostname = ''): 512 | if not htype: 513 | htype = self.TYPE 514 | if raw_data: 515 | Header.__init__(self, htype, hlen, raw_data) 516 | else: 517 | self.hostname = hostname 518 | 519 | def parse(self): 520 | """Parse data""" 521 | self.hostname = self.data 522 | 523 | def __repr__(self): 524 | return "LastSaveHost" + Header.__repr__(self) 525 | 526 | def __str__(self): 527 | return "LastSaveHostHeader(%r)" % self.hostname 528 | 529 | def serial(self): 530 | return self.hostname 531 | 532 | class DBNameHeader(Header): 533 | """ Name of the database 534 | dbName String 535 | """ 536 | TYPE = 0x09 537 | FIELD = 'dbName' 538 | 539 | def __init__(self, htype = None, hlen = 1, raw_data = None, dbName = ''): 540 | if not htype: 541 | htype = self.TYPE 542 | if raw_data: 543 | Header.__init__(self, htype, hlen, raw_data) 544 | else: 545 | self.dbName = dbName 546 | 547 | def parse(self): 548 | """Parse data""" 549 | self.dbName = self.data 550 | 551 | def __repr__(self): 552 | return "DBName" + Header.__repr__(self) 553 | 554 | def __str__(self): 555 | return "DBNameHeader(%r)" % self.dbName 556 | 557 | def serial(self): 558 | return self.dbName 559 | 560 | 561 | class NamedPasswordPolicy(dict): 562 | """ """ 563 | def __init__( 564 | self, 565 | name, 566 | useLowercase = True, 567 | useUppercase = True, 568 | useDigits = True, 569 | useSymbols = True, 570 | useHexDigits = False, 571 | useEasyVision = False, 572 | makePronounceable = False, 573 | minTotalLength = 12, 574 | minLowercaseCharCount = 1, 575 | minUppercaseCharCount = 1, 576 | minDigitCount = 1, 577 | minSpecialCharCount = 1, 578 | allowedSpecialSymbols = DEFAULT_SPECIAL_CHARS, 579 | ): 580 | dict.__init__( 581 | self, 582 | name = name, 583 | useLowercase = useLowercase, 584 | useUppercase = useUppercase, 585 | useDigits = useDigits, 586 | useSymbols = useSymbols, 587 | useHexDigits = useHexDigits, 588 | useEasyVision = useEasyVision, 589 | makePronounceable = makePronounceable, 590 | minTotalLength = minTotalLength, 591 | minLowercaseCharCount = minLowercaseCharCount, 592 | minUppercaseCharCount = minUppercaseCharCount, 593 | minDigitCount = minDigitCount, 594 | minSpecialCharCount = minSpecialCharCount, 595 | allowedSpecialSymbols = allowedSpecialSymbols, 596 | ) 597 | # TODO: Add __repr__ and __str__ 598 | 599 | def __getattribute__(self, attr): 600 | if attr in self: 601 | return self[attr] 602 | else: 603 | return dict.__getattribute__(self, attr = attr) 604 | 605 | def __setattr__(self, attr, value): 606 | if attr in self: 607 | self[attr] = value 608 | else: 609 | return dict.__setattr__(self, attr, value) 610 | 611 | 612 | class NamedPasswordPoliciesHeader(Header): 613 | """ Named password policies 614 | 615 | """ 616 | TYPE = 0x10 617 | FIELD = 'namedPasswordPolicies' 618 | # A few constants 619 | USELOWERCASE = 0x8000 620 | USEUPPERCASE = 0x4000 621 | USEDIGITS = 0x2000 622 | USESYMBOLS = 0x1000 623 | USEHEXDIGITS = 0x0800 624 | USEEASYVERSION = 0x0400 625 | MAKEPRONOUNCEABLE = 0x0200 626 | UNUSED = 0x01ff 627 | 628 | def __init__(self, htype = None, hlen = 1, raw_data = None, namedPasswordPolicies = []): 629 | if not htype: 630 | htype = self.TYPE 631 | if raw_data: 632 | Header.__init__(self, htype, hlen, raw_data) 633 | else: 634 | # Need an order for crypto checks 635 | self.namedPasswordPolicies = [] 636 | for policy in namedPasswordPolicies: 637 | if isinstance(policy, NamedPasswordPolicy): 638 | self.namedPasswordPolicies.append(policy) 639 | elif isinstance(policy, dict): 640 | self.namedPasswordPolicies.append(NamedPasswordPolicy(**policy)) 641 | else: 642 | raise ValueError("Expected a dict or NamedPasswordPolicy") 643 | 644 | def parse(self): 645 | """Parse data""" 646 | self.namedPasswordPolicies = [] 647 | left = self.data 648 | # print repr(left) 649 | count = int(unpack('=2s', left[:2])[0], 16) 650 | log.debug("Should have %r records", count) 651 | left = left[2:] 652 | while len(left) > 2: 653 | count -= 1 654 | if count < 0: 655 | log.warn("More record data than expected") 656 | nameLen = int(unpack('=2s', left[:2])[0], 16) 657 | left = left[2:] 658 | name = left[:nameLen] 659 | log.debug("Name len: %r Name: %r", nameLen, name) 660 | left = left[nameLen:] 661 | policy = unpack('=4s3s3s3s3s3s2s', left[:21]) 662 | left = left[21:] 663 | # str hex to int 664 | policy = [int(x, 16) for x in policy] 665 | log.debug("%r: Policy=%r", name, policy) 666 | # (flags, ttllen, minlow, minup, mindig, minsym, specialCharsLen) = policy 667 | (flags, ttllen, mindig, minlow, minsym, minup, specialCharsLen) = policy 668 | 669 | if flags & self.USELOWERCASE: 670 | uselowercase = True 671 | else: 672 | uselowercase = False 673 | if flags & self.USEUPPERCASE: 674 | useuppercase = True 675 | else: 676 | useuppercase = False 677 | if flags & self.USEDIGITS: 678 | usedigits = True 679 | else: 680 | usedigits = False 681 | if flags & self.USESYMBOLS: 682 | usesymbols = True 683 | else: 684 | usesymbols = False 685 | if flags & self.USEHEXDIGITS: 686 | usehex = True 687 | else: 688 | usehex = False 689 | if flags & self.USEEASYVERSION: 690 | useeasy = True 691 | else: 692 | useeasy = False 693 | if flags & self.MAKEPRONOUNCEABLE: 694 | makepron = True 695 | else: 696 | makepron = False 697 | if specialCharsLen == 0: 698 | if useeasy: 699 | specialChars = DEFAULT_EASY_SPECIAL_CHARS 700 | else: 701 | specialChars = DEFAULT_SPECIAL_CHARS 702 | else: 703 | specialChars = left[:specialCharsLen] 704 | left = left[:specialCharsLen] 705 | self.namedPasswordPolicies.append(NamedPasswordPolicy( 706 | name, 707 | useLowercase = uselowercase, 708 | useUppercase = useuppercase, 709 | useDigits = usedigits, 710 | useSymbols = usesymbols, 711 | useHexDigits = usehex, 712 | useEasyVision = useeasy, 713 | makePronounceable = makepron, 714 | minTotalLength = ttllen, 715 | minLowercaseCharCount = minlow, 716 | minUppercaseCharCount = minup, 717 | minDigitCount = mindig, 718 | minSpecialCharCount = minsym, 719 | allowedSpecialSymbols = specialChars, 720 | )) 721 | log.debug("Policy: %r", self.namedPasswordPolicies[-1]) 722 | log.debug("%r leftover", left) 723 | 724 | def __repr__(self): 725 | return "NamedPasswordPolicies" + Header.__repr__(self) 726 | 727 | def __str__(self): 728 | return "NamedPasswordPolicies(count=%d)" % len(self.namedPasswordPolicies) 729 | 730 | def serial(self): 731 | ret = '%02x' % len(self.namedPasswordPolicies) 732 | for policy in self.namedPasswordPolicies: 733 | flags = 0 734 | if policy.useLowercase: 735 | flags = flags | self.USELOWERCASE 736 | if policy.useUppercase: 737 | flags = flags | self.USEUPPERCASE 738 | if policy.useDigits: 739 | flags = flags | self.USEDIGITS 740 | if policy.useSymbols: 741 | flags = flags | self.USESYMBOLS 742 | if policy.useHexDigits: 743 | flags = flags | self.USEHEXDIGITS 744 | if policy.useEasyVision: 745 | flags = flags | self.USEEASYVERSION 746 | if policy.makePronounceable: 747 | flags = flags | self.MAKEPRONOUNCEABLE 748 | if policy.useEasyVision and policy.allowedSpecialSymbols == DEFAULT_EASY_SPECIAL_CHARS: 749 | allowedSpecialSymbols = '' 750 | elif not policy.useEasyVision and policy.allowedSpecialSymbols == DEFAULT_SPECIAL_CHARS: 751 | allowedSpecialSymbols = '' 752 | else: 753 | allowedSpecialSymbols = policy.allowedSpecialSymbols 754 | ret += '%02x%s%04x%03x%03x%03x%03x%03x%s' % ( 755 | len(policy.name), 756 | policy.name, 757 | flags, 758 | policy.minTotalLength, 759 | policy.minLowercaseCharCount, 760 | policy.minUppercaseCharCount, 761 | policy.minDigitCount, 762 | policy.minSpecialCharCount, 763 | allowedSpecialSymbols, 764 | ) 765 | # psafe_logger.debug("Serial to %s data %s"%(repr(ret),repr(self.data))) 766 | 767 | return ret 768 | 769 | 770 | class DBDescHeader(Header): 771 | """ Description of the database 772 | dbDesc String 773 | """ 774 | TYPE = 0x0a 775 | FIELD = 'dbDesc' 776 | 777 | def __init__(self, htype = None, hlen = 1, raw_data = None, dbDesc = ''): 778 | if not htype: 779 | htype = self.TYPE 780 | if raw_data: 781 | Header.__init__(self, htype, hlen, raw_data) 782 | else: 783 | self.dbDesc = dbDesc 784 | 785 | def parse(self): 786 | """Parse data""" 787 | self.dbDesc = self.data 788 | 789 | def __repr__(self): 790 | return "DBDesc" + Header.__repr__(self) 791 | 792 | def __str__(self): 793 | return "DBDescHeader(%r)" % self.dbDesc 794 | 795 | def serial(self): 796 | return self.dbDesc 797 | 798 | 799 | class DBFiltersHeader(Header): 800 | """ Description of the database 801 | dbDesc String 802 | Specfic filters for this database. This is the text equivalent to 803 | the XML export of the filters as defined by the filter schema. The text 804 | 'image' has no 'print formatting' e.g. tabs and carraige return/line feeds, 805 | since XML processing does not require this. This field was introduced in 806 | format version 0x0305. 807 | """ 808 | TYPE = 0x0b 809 | FIELD = 'dbFilter' 810 | 811 | def __init__(self, htype = None, hlen = 1, raw_data = None, dbFilter = ''): 812 | if not htype: 813 | htype = self.TYPE 814 | if raw_data: 815 | Header.__init__(self, htype, hlen, raw_data) 816 | else: 817 | self.dbFilter = dbFilter 818 | 819 | def parse(self): 820 | """Parse data""" 821 | self.dbFilter = self.data 822 | 823 | def __repr__(self): 824 | return "DBFilters" + Header.__repr__(self) 825 | 826 | def __str__(self): 827 | return "DBFiltersHeader(%r)" % self.dbFilter 828 | 829 | def serial(self): 830 | return self.dbFilter 831 | 832 | 833 | class RecentEntriesHeader(Header): 834 | """ Description of the database 835 | recentEntries List of UUIDs 836 | 837 | A list of the UUIDs (32 hex character representation of the 16 byte field) 838 | of the recently used entries, prefixed by a 2 hex character representation 839 | of the number of these entries (right justified and left filled with zeroes). 840 | The size of the number of entries field gives a maximum number of entries of 255, 841 | however the GUI may impose further restrictions e.g. Windows MFC UI limits this 842 | to 25. The first entry is the most recent entry accessed. This field was 843 | introduced in format version 0x0307. 844 | """ 845 | TYPE = 0x0f 846 | FIELD = 'recentEntries' 847 | 848 | def __init__(self, htype = None, hlen = 1, raw_data = None, recentEntries = []): 849 | if not htype: 850 | htype = self.TYPE 851 | if raw_data: 852 | Header.__init__(self, htype, hlen, raw_data) 853 | else: 854 | self.recentEntries = recentEntries 855 | 856 | def parse(self): 857 | """Parse data""" 858 | LEN = 32 859 | left = self.data 860 | assert len(left) % LEN == 2 861 | self.recentEntries = [] 862 | count = int(unpack('=2s', left[:2])[0], 16) 863 | log.debug("Should have %r records", count) 864 | left = left[2:] 865 | while len(left) >= LEN: 866 | count -= 1 867 | if count < 0: 868 | log.warn("More record data than expected") 869 | segement = left[:LEN] 870 | left = left[LEN:] 871 | log.debug("Working with %r", segement) 872 | found = UUID(segement) 873 | log.debug("Found UUID of %r", found) 874 | self.recentEntries.append(found) 875 | log.debug("Left over: %r", left) 876 | 877 | def __repr__(self): 878 | return "RecentEntries" + Header.__repr__(self) 879 | 880 | def __str__(self): 881 | return "RecentEntriesHeader(%r)" % self.recentEntries 882 | 883 | def serial(self): 884 | packed = [pack('=16s', str(uuid.bytes)) for uuid in self.recentEntries[:256]] 885 | return ','.join(packed) 886 | 887 | 888 | class EmptyGroupHeader(Header): 889 | """ An empty group - May appear multiple times. 890 | groupName Group name 891 | 892 | This fields contains the name of an empty group that cannot be constructed 893 | from entries within the database. Unlike other header fields, this field can appear 894 | multiple times. 895 | """ 896 | TYPE = 0x11 897 | FIELD = 'groupName' 898 | 899 | def __init__(self, htype = None, hlen = 1, raw_data = None, groupName = ''): 900 | if not htype: 901 | htype = self.TYPE 902 | if raw_data: 903 | Header.__init__(self, htype, hlen, raw_data) 904 | else: 905 | self.groupName = groupName 906 | 907 | def parse(self): 908 | """Parse data""" 909 | self.groupName = self.data 910 | 911 | def __repr__(self): 912 | return "EmptyGroup" + Header.__repr__(self) 913 | 914 | def __str__(self): 915 | return "EmptyGroupHeader(%r)" % self.groupName 916 | 917 | def serial(self): 918 | return self.groupName 919 | 920 | 921 | class EOFHeader(Header): 922 | """End of headers 923 | >>> x=EOFHeader(255,0,'\x00\x00\x00\x00\xff\xbc_AP\x10\xf19\xae\xe99g') 924 | >>> repr(x) 925 | "EOFHeader(255,0,'\\x00\\x00\\x00\\x00\\xff\\xbc_AP\\x10\\xf19\\xae\\xe99g')" 926 | >>> str(x) 927 | 'EOF' 928 | >>> x.serial() 929 | '' 930 | 931 | """ 932 | TYPE = 0xff 933 | data = '' 934 | def __init__(self, htype = None, hlen = 0, raw_data = ''): 935 | if not htype: 936 | htype = self.TYPE 937 | if raw_data: 938 | Header.__init__(self, htype, hlen, raw_data) 939 | else: 940 | pass 941 | 942 | def __repr__(self): 943 | return "EOF" + Header.__repr__(self) 944 | 945 | def __str__(self): 946 | return "EOF" 947 | 948 | def Create_Header(fetchblock_f): 949 | """Returns a header object. Uses fetchblock_f to read a 16 byte chunk of data 950 | fetchblock_f(number of blocks) 951 | """ 952 | firstblock = fetchblock_f(1) 953 | log.debug("Header of header: %s" % repr(firstblock[:5])) 954 | (rlen, rtype) = unpack('=lc', firstblock[:5]) 955 | rtype = ord(rtype) 956 | data = firstblock[5:] 957 | log.debug("Rtype: %s Len: %s" % (rtype, rlen)) 958 | if rlen > len(data): 959 | data += fetchblock_f(((rlen - len(data) - 1) / 16) + 1) 960 | assert rlen <= len(data) 961 | # TODO: Clean up header add back 962 | data = firstblock[:5] + data 963 | if headers.has_key(rtype): 964 | return headers[rtype](rtype, rlen, data) 965 | else: 966 | # Unknown header 967 | return Header(rtype, rlen, data) 968 | 969 | if __name__ == "__main__": 970 | import doctest 971 | doctest.testmod() 972 | 973 | --------------------------------------------------------------------------------