├── CHANGELOG ├── LICENSE.md ├── README.md ├── bin ├── keepassc ├── keepassc-agent └── keepassc-server ├── keepassc-agent.1 ├── keepassc-server.1 ├── keepassc.1 ├── keepassc ├── __init__.py ├── agent.py ├── client.py ├── conn.py ├── control.py ├── daemon.py ├── dbbrowser.py ├── editor.py ├── filebrowser.py ├── helper.py └── server.py └── setup.py /CHANGELOG: -------------------------------------------------------------------------------- 1 | Version 1.8 (Mai 16, 2018) 2 | 3 | Changed crypto package to PyCryptodome 4 | Relicensed with the ISC license 5 | 6 | Version 1.7 (January 05, 2015) 7 | 8 | Default expiration dates when changing expiration date or creating new entries. 9 | Automatic line breaks when editing comments. 10 | xscroll in Editor with one line (e.g. when editing titles; was issue #40). 11 | Direct filepath searching with the editor (issue #37). 12 | Use pbcopy instead of xsel on Mac OS X (issue #59). 13 | Including spaces in password generation is optional now. 14 | Bug fixes. 15 | 16 | Version 1.6 (August 16, 2013) 17 | 18 | A server-client structure is implemented for network usage or to omit password entering. SSL/TLS, threading i supported, too. 19 | Some little performance tweaks. 20 | Entry information are not cutted anymore if the window is to small. 21 | Expired entries are marked red. 22 | New help menu. 23 | 24 | Version 1.5 (February 22, 2013) 25 | 26 | Support for moving groups and entries 27 | Password generator 28 | Configuration menu 29 | Autolocking feature with adjustable delay 30 | Self-deleting clipboard now with adjustable delay, too 31 | Full Unicode support 32 | Use of vim-like keys in menus 33 | Support for 'gg', 'G', '/' like in vim/ranger 34 | New editor to edit attributes of entries and groups and for other things, thanks goes to Scott (not for direct filepath, yet) 35 | Go to previous screen from every dialog 36 | Remember last opened database and keyfile and go to open dialog directly (of course everything is optional) 37 | Rearranged menus 38 | xsel is optionally 39 | Write entries unpuffered to console 40 | Annoying message while opening URLs is fixed 41 | Several bugs fixed 42 | 43 | Version 1.4 (November 10, 2012) 44 | 45 | Show groups and entries alphabetically sorted 46 | Man page 47 | Option to show entries on commandline 48 | Search utility (type 'f') 49 | Showing a * if the file has changed 50 | Use of a generalized and commandable menu 51 | Clear the clipboard only if the content of clipboard 52 | was copied from the db and not from other programs 53 | Fixed several bugs 54 | 55 | Version 1.3 56 | 57 | Added support for keyfiles 58 | Can now parse a database through the command line 59 | When entering a filepath auto-completion loops through all possibilities 60 | It's now possible to use KeePassC without X by the possibility of showing the passwords 61 | Fixed several bugs 62 | 63 | Version 1.2 64 | 65 | Fixed a bug relating to auto-completion when entering a direct filepath 66 | Remembering the last opened database 67 | Can now close the program with Ctrl-C 68 | When creating a new database password entering needs a confirmation 69 | 70 | Version 1.1 71 | 72 | Added vim-like navigation 73 | Due to this help button is F1 now 74 | Typing a direct filepath including path-completition is supported 75 | Hidden files are supported 76 | Copy username to clipboard 77 | Open url in standard browser 78 | Fixed bugs (backspace and tmux, os.getlogin(), locking) 79 | 80 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2012-2018, Karsten-Kai König 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KeePassC v.1.8.2 2 | ================ 3 | 4 | * License: ISC 5 | * Author: Karsten-Kai König 6 | ** License of editor.py: MIT 7 | ** Author of editor.py: Scott Hansen 8 | ** Github: https://github.com/firecat53/py_curses_editor 9 | * Stable download: https://github.com/raymontag/keepassc/tarball/master 10 | * Website: http://raymontag.github.com/keepassc 11 | * Bug tracker: https://github.com/raymontag/keepassc/issues?state=open 12 | * Git: git://github.com/raymontag/keepassc.git 13 | 14 | Features: 15 | --------- 16 | KeePassC is a password manager fully compatible to KeePass v.1.x and KeePassX. That is, your 17 | password database is fully encrypted with AES. 18 | 19 | KeePassC is written in Python 3 and comes with a curses-interface. It is completely controlled 20 | with the keyboard. 21 | 22 | Since v.1.6.0 network usage is implemented. 23 | 24 | Install: 25 | -------- 26 | 27 | First check if Python 3 is executed on your system with 'python' (e.g. ArchLinux) or with 'python3' (e.g. Fedora). If the latter applies open bin/keepassc with an editor of your choice and edit the first line to '#!/usr/bin/env python3', if the former do nothing. 28 | 29 | If all dependencies are fulfilled type 'python setup.py install' resp. 'python3 setup.py install' in the root directory of KeePassC. 30 | 31 | Furthermore check if the directory /var/empty exists (normally it should but it seems that is doesn't on Debian and derivates). If not execute as root user 'mkdir -m 755 /var/empty'. 32 | 33 | Usage: 34 | ------ 35 | Start the program with 'keepassc'. To get help type 'F1' while KeePassC is executed and you will see usage 36 | information to the current window (not in main menu). 37 | 38 | For a short introduction have a look at http://raymontag.github.com/keepassc/docu.html. Also use 'man keepassc'. 39 | 40 | For help using the server have a look at http://raymontag.github.com/keepassc/server.html, 'man keepassc-server' and 'man keepassc-agent'. 41 | 42 | You can get help at any time by pressing F1 in the file or database browser. 43 | 44 | Dependencies: 45 | ------------- 46 | 47 | * Python 3 (>= 3.3) 48 | * kppy http://www.nongnu.org/kppy 49 | * 50 | * xsel (optional but necessary if you want to copy usernames and passwords to clipboard) http://www.vergenet.net/~conrad/software/xsel/ 51 | * A POSIX-compatible operating system 52 | 53 | Copyright (c) 2012-2018 Karsten-Kai König 54 | 55 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 56 | 57 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 58 | 59 | -------------------------------------------------------------------------------- /bin/keepassc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import logging 6 | import socket 7 | from curses import wrapper 8 | from getpass import getpass 9 | from os import chdir, geteuid 10 | from os.path import expanduser, realpath, splitext, join 11 | from sys import exit, stdout 12 | 13 | from kppy.database import KPDBv1 14 | from kppy.exceptions import KPError 15 | 16 | from keepassc.conn import * 17 | from keepassc.client import Client 18 | from keepassc.control import Control 19 | 20 | 21 | __doc__ = '''This program gives you access to your KeePass 1.x or 22 | KeePassX databases through a nice curses interface. 23 | 24 | It is completely controllable with the keyboard. 25 | 26 | ''' 27 | 28 | 29 | def arg_parse(): 30 | "Parse the command line arguments" 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument('--asroot', action='store_true', default=False, 33 | help='parse option to execute keepassc as root user') 34 | parser.add_argument('-d', '--database', default=None, 35 | help='Path to database file.') 36 | parser.add_argument('-k', '--keyfile', default=None, 37 | help='Path to keyfile.') 38 | parser.add_argument('-c', '--curses', action='store_true', default=False, 39 | help='Use curses interface while using a remote ' 40 | 'connection.') 41 | parser.add_argument('-as', '--address_server', default='localhost', 42 | help='Server address (not required if using agent)', 43 | type=str) 44 | parser.add_argument('-ps', '--port_server', default=50000, 45 | help='Server port (not required if using agent)', 46 | type=int) 47 | parser.add_argument('-pa', '--port_agent', default=50001, 48 | help='Agent port', type=int) 49 | parser.add_argument('-a', '--agent', action='store_true', default=False, 50 | help='Use agent for remote connection') 51 | parser.add_argument('-dc', '--direct_conn', action='store_true', 52 | default=False, help='Connect directly to server') 53 | parser.add_argument('-s', '--ssl', action='store_true', default=False, 54 | help='Use SSL/TLS') 55 | parser.add_argument('-e', '--entry', help='Print entry with parsed ' 56 | 'title\nYou will see a password prompt; leave it ' 57 | 'blank if you only want to use a key-file\nJust ' 58 | 'type a part of the entry title lower-case, ' 59 | 'it\'s case-insensitive and will search for ' 60 | 'matching string parts\n\n' 61 | 'WARNING: Your passwords will be displayed ' 62 | 'directly on your command line!') 63 | parser.add_argument('-l', '--log_level', default = False, 64 | help='Set logging level for network use. ' 65 | 'Default is ERROR but for ' 66 | 'analyzing network flow INFO could be useful. ' 67 | 'Set it with keepassc [...] -l [...] to ' 68 | 'INFO', action='store_true') 69 | return parser.parse_args() 70 | 71 | 72 | def parse_entry(entry, database, password, keyfile): 73 | """Given the --entry command line option, parse the database and 74 | return a matching entry. 75 | 76 | Args: 77 | entry: the search string 78 | database: path to the database file 79 | password: password result from getpass() 80 | keyfile: path to the keyfile 81 | 82 | """ 83 | 84 | try: 85 | db = KPDBv1(database, password, keyfile) 86 | db.load() 87 | except KPError as err: 88 | print(err) 89 | exit() 90 | for i in db.entries: 91 | if entry.lower() in i.title.lower(): 92 | print('Title: ' + i.title) 93 | if i.url is not None: 94 | stdout.write('URL: ' + i.url + '\n') 95 | if i.username is not None: 96 | stdout.write('Username: ' + i.username + '\n') 97 | if i.password is not None: 98 | stdout.write('Password: ' + i.password + '\n') 99 | if i.creation is not None: 100 | stdout.write('Creation: ' + i.creation.__str__() + '\n') 101 | if i.last_access is not None: 102 | stdout.write('Access: ' + i.last_access.__str__() + '\n') 103 | if i.last_mod is not None: 104 | stdout.write('Modification: ' + i.last_mod.__str__() + '\n') 105 | if i.expire is not None: 106 | stdout.write('Expiration: ' + i.expire.__str__() + '\n') 107 | if i.comment is not None: 108 | stdout.write('Comment: ' + i.comment + '\n\n') 109 | stdout.flush() 110 | 111 | def direct_connection(): 112 | '''Direct connection to a KeePassC-server. 113 | 114 | Establish a direct connection to a KeePassC-server and find 115 | entries. 116 | 117 | ''' 118 | 119 | # Get password 120 | print("Leave blank if you use a keyfile only") 121 | password = getpass() 122 | if password == '' and args.keyfile is None: 123 | print('A password or keyfile is needed!') 124 | exit(0) 125 | elif password == '': 126 | password = None 127 | 128 | 129 | if args.keyfile is not None: 130 | keyfile = realpath(expanduser(args.keyfile)) 131 | else: 132 | keyfile = args.keyfile 133 | 134 | if args.ssl is True: 135 | try: 136 | datapath = realpath(expanduser(getenv('XDG_DATA_HOME'))) 137 | except: 138 | datapath = realpath(expanduser('~/.local/share')) 139 | finally: 140 | tls_dir = join(datapath, 'keepassc') 141 | else: 142 | tls_dir = None 143 | 144 | chdir("/var/empty") 145 | 146 | # Get entry title 147 | if args.entry: 148 | entry = args.entry.encode() 149 | else: 150 | entry = input('Part of title: ').encode() 151 | 152 | if args.log_level is True: 153 | loglevel = logging.INFO 154 | else: 155 | loglevel = logging.ERROR 156 | 157 | # Establish connection and find entry 158 | client = Client(loglevel, 'client.log', args.address_server, 159 | args.port_server, password, args.keyfile, 160 | args.ssl, tls_dir) 161 | 162 | data = client.find(entry) 163 | if data[:4] == 'FAIL': 164 | print(data) 165 | exit(0) 166 | 167 | data = data.split('\n') 168 | for i in data: 169 | stdout.write(i + '\n') 170 | stdout.flush() 171 | 172 | def use_agent(): 173 | '''Use the KeePassC-agent to find entries on a server.''' 174 | 175 | try: 176 | logdir = realpath(expanduser(getenv('XDG_DATA_HOME'))) 177 | except: 178 | logdir = realpath(expanduser('~/.local/share')) 179 | finally: 180 | logfile = join(logdir, 'keepassc', 'client.log') 181 | 182 | if args.log_level is True: 183 | loglevel = logging.INFO 184 | else: 185 | loglevel = logging.ERROR 186 | 187 | logging.basicConfig(format='[%(levelname)s] in %(filename)s:' 188 | '%(funcName)s at %(asctime)s\n%(message)s', 189 | level=loglevel, filename=logfile, 190 | filemode='a') 191 | 192 | # Get entry title 193 | if args.entry: 194 | entry = args.entry.encode() 195 | else: 196 | entry = input('Part of title: ').encode() 197 | 198 | # Establish connect to agent 199 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 200 | sock.settimeout(60) 201 | try: 202 | sock.connect(('localhost', args.port_agent)) 203 | # Init sequence 204 | sendmsg(sock, build_message((b'FIND', entry))) 205 | except OSError as err: 206 | print(err.__str__()) 207 | exit(0) 208 | 209 | answer = receive(sock).decode() 210 | if answer[:4] == 'FAIL': 211 | print(answer) 212 | exit(0) 213 | 214 | answer = answer.split('\n') 215 | for i in answer: 216 | stdout.write(i + '\n') 217 | stdout.flush() 218 | 219 | if __name__ == '__main__': 220 | args = arg_parse() 221 | if geteuid() == 0 and args.asroot is False: 222 | print('If you really want to execute this program as root user type ' 223 | '\'keepassc --asroot\'') 224 | print('Warning: This will annul a security concept of keepassc') 225 | else: 226 | if args.direct_conn is True: 227 | direct_connection() 228 | elif args.agent is True: 229 | use_agent() 230 | elif args.entry: 231 | password = getpass() 232 | if password == '': 233 | password = None 234 | db = realpath(expanduser(args.database)) 235 | if args.keyfile is not None: 236 | keyfile = realpath(expanduser(args.keyfile)) 237 | else: 238 | keyfile = args.keyfile 239 | chdir("/var/empty") 240 | parse_entry(args.entry, db, password, keyfile) 241 | elif args.curses: 242 | app = Control() 243 | wrapper(app.main_loop(remote=True)) 244 | elif args.database and splitext(args.database)[-1] == '.kdb': 245 | filepath = realpath(expanduser(args.database)) 246 | app = Control() 247 | wrapper(app.main_loop(filepath)) 248 | else: 249 | app = Control() 250 | wrapper(app.main_loop()) 251 | -------------------------------------------------------------------------------- /bin/keepassc-agent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | from getpass import getpass 6 | from os import getenv, geteuid 7 | from os.path import expanduser, realpath, join 8 | 9 | from keepassc.daemon import Daemon 10 | from keepassc.agent import Agent 11 | 12 | def arg_parse(): 13 | "Parse the command line arguments" 14 | 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument('-k', '--keyfile', default=None, 17 | help='Path to keyfile.', type=str) 18 | parser.add_argument('-as', '--address', default='localhost', 19 | help='Address for the server.', type=str) 20 | parser.add_argument('-ps', '--port', default=50000, 21 | help='Port for the server.', type=int) 22 | parser.add_argument('-pc', '--port_agent', default=50001, 23 | help='Port for the agent.', type=int) 24 | parser.add_argument('-l', '--log_level', default = False, 25 | help='Set logging level. Default is ERROR but for' 26 | 'analyzing network flow INFO could be useful. ' 27 | 'Set it with keepassc-agent [...] -l [...] to ' 28 | 'INFO', action='store_true') 29 | parser.add_argument('-s', '--ssl', default=False, 30 | help='Use SSL/TLS.', action='store_true') 31 | parser.add_argument('cmd', default=None, 32 | help='Daemon command: start|stop|restart', type=str) 33 | return parser.parse_args() 34 | 35 | if __name__ == '__main__': 36 | args = arg_parse() 37 | if geteuid() == 0 and args.asroot is False: 38 | print('If you really want to execute this program as root user type ' 39 | '\'keepassc --asroot\'') 40 | print('Warning: This will annul a security concept of keepassc') 41 | else: 42 | try: 43 | datapath = realpath(expanduser(getenv('XDG_DATA_HOME'))) 44 | except: 45 | datapath = realpath(expanduser('~/.local/share')) 46 | finally: 47 | pidfile = join(datapath, 'keepassc', 'agent.pid') 48 | if args.ssl is True: 49 | tls_dir = join(datapath, 'keepassc') 50 | else: 51 | tls_dir = None 52 | 53 | if args.cmd == 'start': 54 | if args.log_level is True: 55 | loglevel = logging.INFO 56 | else: 57 | loglevel = logging.ERROR 58 | 59 | print("Leave blank if you use a keyfile only") 60 | password = getpass() 61 | if password == '': 62 | password = None 63 | 64 | agent = Agent(pidfile, loglevel, 'agent.log', args.address, args.port, 65 | args.port_agent, password, args.keyfile, args.ssl, 66 | tls_dir) 67 | agent.start() 68 | elif args.cmd == 'stop': 69 | daemon = Daemon(pidfile) 70 | daemon.stop() 71 | 72 | -------------------------------------------------------------------------------- /bin/keepassc-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import logging 5 | import sys 6 | from getpass import getpass 7 | from os import getenv, geteuid 8 | from os.path import expanduser, realpath, join 9 | 10 | from keepassc.daemon import Daemon 11 | from keepassc.server import Server 12 | 13 | def arg_parse(): 14 | "Parse the command line arguments" 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument('--asroot', action='store_true', default=False, 17 | help='parse option to execute keepassc as root user') 18 | parser.add_argument('-d', '--database', default=None, 19 | help='Path to database file.', type=str) 20 | parser.add_argument('-k', '--keyfile', default=None, 21 | help='Path to keyfile.', type=str) 22 | parser.add_argument('-a', '--address', default=None, 23 | help='Address for the server.', type=str) 24 | parser.add_argument('-p', '--port', default=50002, 25 | help='Port for the server.', type=int) 26 | parser.add_argument('-ps', '--port_tls', default=50003, 27 | help='Port for server\'s TLS-port if -S is used.', 28 | type=int) 29 | parser.add_argument('-l', '--log_level', default = False, 30 | help='Set logging level. Default is ERROR but for' 31 | 'analyzing network flow INFO could be useful. ' 32 | 'Set it with keepassc-server [...] -l [...] to ' 33 | 'INFO', action='store_true') 34 | parser.add_argument('-s', '--ssl', default=False, 35 | help='Use SSL/TLS additionaly.', action='store_true') 36 | parser.add_argument('-S', '--ssl_req', default=False, 37 | help='Use SSL/TLS only.', action='store_true') 38 | parser.add_argument('cmd', default=None, 39 | help='Daemon command: start|stop', type=str) 40 | return parser.parse_args() 41 | 42 | if __name__ == '__main__': 43 | args = arg_parse() 44 | if geteuid() == 0 and args.asroot is False: 45 | print('If you really want to execute this program as root user type ' 46 | '\'keepassc --asroot\'') 47 | print('Warning: This will annul a security concept of keepassc') 48 | else: 49 | try: 50 | datapath = realpath(expanduser(getenv('XDG_DATA_HOME'))) 51 | except: 52 | datapath = realpath(expanduser('~/.local/share')) 53 | finally: 54 | pidfile = join(datapath, 'keepassc', 'server.pid') 55 | if args.ssl is True or args.ssl_req is True: 56 | tls_dir = join(datapath, 'keepassc') 57 | else: 58 | tls_dir = None 59 | 60 | if args.cmd == 'start': 61 | if args.ssl and args.port == args.port_tls: 62 | print("Non-SSL-prt and SSL-port must be different.") 63 | sys.exit(1) 64 | if args.log_level is True: 65 | loglevel = logging.INFO 66 | else: 67 | loglevel = logging.ERROR 68 | 69 | if args.database is None: 70 | print('Need database path!') 71 | sys.exit(1) 72 | print("Leave blank if you use a keyfile only") 73 | password = getpass() 74 | if password == '': 75 | password = None 76 | server = Server(pidfile, loglevel, 'server.log', args.address, 77 | args.port, args.database, password, args.keyfile, 78 | args.ssl, tls_dir, args.port_tls, args.ssl_req) 79 | server.start() 80 | elif args.cmd == 'stop': 81 | daemon = Daemon(pidfile) 82 | daemon.stop() 83 | 84 | -------------------------------------------------------------------------------- /keepassc-agent.1: -------------------------------------------------------------------------------- 1 | .TH KeePassC v.1.6.2 2 | .SH NAME 3 | KeePassC \- KeePassC is a curses-based password manager compatible to KeePass v.1.x and KeePassX 4 | .PP 5 | This manpage describes the agent. 6 | .SH SYNOPSIS 7 | keepassc-agent [options] [cmd] 8 | .SH DESCRIPTION 9 | Since v.1.6.0 network usage is implemented. This manpage describes how to use the agent-daemon. 10 | .SH USAGE 11 | For a short introduction have a look at http://raymontag.github.com/keepassc/server.html. 12 | .PP 13 | You start the agent with 'keepassc-agent start'. You will be prompted for a password. If you need a keyfile use the -k option. 14 | .PP 15 | In the case above the agent is binded to 'localhost:50001'. The alwas binds always to localhost. If you want to use another port use -pc. 16 | .PP 17 | The agent uses 'localhost:50000' for the server address by default. If you want to use another server address use -as and -ps. 18 | .PP 19 | The agent is implemented as a daemon. Therefore commands to start and stop the agent are needed. 20 | .SH COMMANDS 21 | .TP 22 | .B start 23 | Start the agent-daemon. 24 | .TP 25 | .B stop 26 | Stop the agent-daemon. 27 | .SH OPTIONS 28 | .TP 29 | .B -h, --help 30 | show the help message and exit 31 | .TP 32 | .B --asroot 33 | Execute server as root user. 34 | .TP 35 | .B -k KEYFILE, --keyfile KEYFILE 36 | Path to keyfile. 37 | .TP 38 | .B -as ADDRESS, --address ADDRESS 39 | Address for the server. 40 | .TP 41 | .B -ps PORT, --port PORT 42 | Port for the server. 43 | .TP 44 | .B -pc PORT_AGENT, --port_agent PORT_AGENT 45 | Port for the agent. 46 | .TP 47 | .B -l, --log_level 48 | Set logging level. Default is ERROR but foranalyzing 49 | network flow INFO could be useful. Set it with 50 | keepassc-server [...] -l [...] to INFO 51 | .TP 52 | .B -s, --ssl 53 | Use SSL/TLS. 54 | .SH AUTHOR 55 | Karsten-Kai König 56 | .SH LICENSE 57 | KeePassC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or at your option) any later version. 58 | .PP 59 | KeePassC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 60 | .PP 61 | You should have received a copy of the GNU General Public License along with KeePassC. If not, see . 62 | -------------------------------------------------------------------------------- /keepassc-server.1: -------------------------------------------------------------------------------- 1 | .TH KeePassC v.1.6.2 2 | .SH NAME 3 | KeePassC \- KeePassC is a curses-based password manager compatible to KeePass v.1.x and KeePassX 4 | .PP 5 | This manpage describes the server. 6 | .SH SYNOPSIS 7 | keepassc-server [options] [cmd] 8 | .SH DESCRIPTION 9 | Since v.1.6.0 network usage is implemented. This manpage describes how to use the server-daemon. The server is not for network-usage only. You can use it to omit password entering by using the agent additionaly, too. 10 | .SH USAGE 11 | For a short introduction have a look at http://raymontag.github.com/keepassc/server.html. 12 | .PP 13 | You start the server with 'keepassc-server -d /path/to/database start'. The database path is always needed. You will be prompted for a password. If you need a keyfile use the -k option. 14 | .PP 15 | In the case above the server is binded to 'localhost:50000'. The server binds always to this address even if you specify a network address. The latter can be done by the -a option. The standard port is 50002 for network use. If you want another use -p. 16 | .PP 17 | If you just use -a the communication between the server and the client is plain text. If you want to use TLS use 18 | -s or -S (for TLS only). A port for the TLS connection can be specified by -ps, standard is 50003. For a tutorial how to create TLS certificates scroll down. 19 | .SH COMMANDS 20 | The server is implemented as a daemon. Therefore commands to start and stop the server are needed. 21 | .TP 22 | .B start 23 | Start the server-daemon. Additional options are needed. 24 | .TP 25 | .B stop 26 | Stop the server-daemon. 27 | .SH OPTIONS 28 | .TP 29 | .B -h, --help 30 | show the help message and exit 31 | .TP 32 | .B --asroot 33 | Execute server as root user. 34 | .TP 35 | .B -d DATABASE, --database DATABASE 36 | Path to database file. 37 | .TP 38 | .B -k KEYFILE, --keyfile KEYFILE 39 | Path to keyfile. 40 | .TP 41 | .B -a ADDRESS, --address ADDRESS 42 | Address for the server. 43 | .TP 44 | .B -p PORT, --port PORT 45 | Port for the server. 46 | .TP 47 | .B -ps PORT_TLS, --port_tls PORT_TLS 48 | Port for server's TLS-port if -S is used. 49 | .TP 50 | .B -l, --log_level 51 | Set logging level. Default is ERROR but foranalyzing 52 | network flow INFO could be useful. Set it with 53 | keepassc-server [...] -l [...] to INFO 54 | .TP 55 | .B -s, --ssl 56 | Use SSL/TLS additionaly. 57 | .TP 58 | .B -S, --ssl_req 59 | Use SSL/TLS only. 60 | .SH USING TLS (formally SSL) 61 | To use TLS when using keepassc-server you have to generate a server certificate. This is a manual how to do this: 62 | .PP 63 | First you need openssl. If you've installed it make a new directory to execute the following steps. Now lets generate the root certificate: 64 | .TP 65 | .B # openssl req -new -x509 -newkey rsa:4096 -keyout cakey.pem -out cacert.pem -days 3650 66 | TLS works with an asymmetric cipher. Therefore you've to take care of cakey.pem (the private key)! Otherwise everybody could generate it's own certificates and could phish your passphrase credentials. A strong password for the private key is self-evident. E.g. move the key after this whole procedure on a flash drive. You should never store the private key on the server. 67 | 68 | The other fields are not necessary (for KeePassC. For other uses it could but that's not the topic). The last parameter specifies that the root certificate is valid for 10 years. You can change this if you need to. 69 | .PP 70 | To check if you could open the key do 71 | .TP 72 | .B # openssl rsa -in cakey.pem -noout -text 73 | .PP 74 | and type your password. 75 | .PP 76 | Now we generate the certificate for the server. 77 | .TP 78 | .B # openssl genrsa -out serverkey.pem -aes256 4096 -days 3650 79 | .TP 80 | .B # openssl rsa -in serverkey.pem -out serverkey.pem 81 | Take care of serverkey.pem! It's a private key, too and with this key everybody could act as your server. 82 | .PP 83 | Now we'll sign this key with the root certificate. 84 | .TP 85 | .B # openssl req -new -key serverkey.pem -out req.pem -nodes 86 | It is essential that you type "KeePassC Server" for "Common Name". Otherwise the client would never accept the server. Everything else is not necessary and could be empty. 87 | .PP 88 | Now change your openssl configuration: 89 | .TP 90 | .B /etc/ssl/openssl.cnf 91 | dir = . 92 | 93 | new_certs_dir = $dir 94 | 95 | private_key = $dir/cakey.pem 96 | 97 | RANDFILE = $dir/.rand 98 | 99 | default_days = 3650 100 | 101 | stateOrProvinceName = optional 102 | .PP 103 | Now do 104 | .TP 105 | .B # echo 01 > serial 106 | .TP 107 | .B # touch index.txt 108 | Now we do the final step: We sign the certificate: 109 | .TP 110 | .B openssl ca -in req.pem -notext -out servercert.pem 111 | You need to type the password of the root certificate. 112 | .PP 113 | To install the certificates move servercert.pem and serverkey.pem into .local/share/keepassc/ or any other directory specified by XDG_DATA_HOME with keepassc as subfolder on the server. 114 | .PP 115 | On the client side you move cacert.pem into the same folder. You're now ready to use TLS with KeePassC. 116 | .SH AUTHOR 117 | Karsten-Kai König 118 | .SH LICENSE 119 | KeePassC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or at your option) any later version. 120 | .PP 121 | KeePassC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 122 | .PP 123 | You should have received a copy of the GNU General Public License along with KeePassC. If not, see . 124 | -------------------------------------------------------------------------------- /keepassc.1: -------------------------------------------------------------------------------- 1 | .TH KeePassC v.1.6.2 2 | .SH NAME 3 | KeePassC \- KeePassC is a curses-based password manager compatible to KeePass v.1.x and KeePassX 4 | .SH SYNOPSIS 5 | keepassc [options] 6 | .SH DESCRIPTION 7 | KeePassC is a password manager fully compatible to KeePass v.1.x and KeePassX. That is, your password database is fully encrypted with AES. 8 | .PP 9 | KeePassC is written in Python 3 and comes with a curses-interface. It is completely controlled with the keyboard. 10 | .PP 11 | KeePassC provides everything to read and edit the password databases. Watch http://raymontag.github.com/keepassc/ for a full feature list. 12 | .SH USAGE 13 | For a short introduction have a look at http://raymontag.github.com/keepassc/docu.html. 14 | .PP 15 | For help using the server have a look at http://raymontag.github.com/keepassc/server.html or at 'man keepassc-server' and 'man keepassc-agent'. 16 | .PP 17 | You can get help at any time by pressing F1 in the editor, file or database browser. 18 | .SH USAGE AS A CLIENT 19 | If you want to connect to a remote database created by 'keepassc-server' you can use 'keepassc' as a client. 20 | .PP 21 | To list entries on the command line similar to -e use -dc. In this case the client will connect to 'localhost:50000'. If you want to connect to another server you can specify his address by -as and -ps. To use TLS use -s. If you want to use the normal KeePassC-interface use -c with the named options. 22 | .PP 23 | Furthermore you can use the agent by using -a. 24 | .SH OPTIONS 25 | .TP 26 | .B -h, --help 27 | show this help message and exit 28 | .TP 29 | .B --asroot 30 | parse option to execute keepassc as root user 31 | .TP 32 | .B -d DATABASE, --database DATABASE 33 | Path to database file. 34 | .TP 35 | .B -k KEYFILE, --keyfile KEYFILE 36 | Path to keyfile. 37 | .TP 38 | .B -c, --curses 39 | Use curses interface while using a remote connection. 40 | .TP 41 | .B -as ADDRESS_SERVER, --address_server ADDRESS_SERVER 42 | Server address (not required if using agent) 43 | .TP 44 | .B -ps PORT_SERVER, --port_server PORT_SERVER 45 | Server port (not required if using agent) 46 | .TP 47 | .B -pa PORT_AGENT, --port_agent PORT_AGENT 48 | Agent port 49 | .TP 50 | .B -a, --agent 51 | Use agent for remote connection 52 | .TP 53 | .B -dc, --direct_conn 54 | Connect directly to server 55 | .TP 56 | .B -s, --ssl 57 | Use SSL/TLS 58 | .TP 59 | .B -e ENTRY, --entry ENTRY 60 | Print entry with parsed title You will see a password 61 | prompt; leave it blank if you only want to use a key- 62 | file Just type a part of the entry title lower-case, 63 | it's case-insensitive and will search for matching 64 | string parts WARNING: Your passwords will be displayed 65 | directly on your command line! 66 | .TP 67 | .B -l, --log_level 68 | Set logging level for network use. Default is ERROR 69 | but for analyzing network flow INFO could be useful. 70 | Set it with keepassc [...] -l [...] to INFO 71 | .SH AUTHOR 72 | Karsten-Kai König 73 | .SH LICENSE 74 | KeePassC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or at your option) any later version. 75 | .PP 76 | KeePassC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 77 | .PP 78 | You should have received a copy of the GNU General Public License along with KeePassC. If not, see . 79 | -------------------------------------------------------------------------------- /keepassc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raymontag/keepassc/3a3c7ef7b3ee1ceb16b613176d54dad89c0408df/keepassc/__init__.py -------------------------------------------------------------------------------- /keepassc/agent.py: -------------------------------------------------------------------------------- 1 | """This module implements an agent for KeePassC 2 | 3 | Classes: 4 | Agent(Client, Daemon) 5 | """ 6 | 7 | import logging 8 | import signal 9 | import socket 10 | import ssl 11 | import sys 12 | from hashlib import sha256 13 | from os import chdir 14 | from os.path import expanduser, realpath, isfile, join 15 | 16 | from keepassc.conn import * 17 | from keepassc.client import Client 18 | from keepassc.daemon import Daemon 19 | 20 | 21 | class Agent(Daemon): 22 | """The KeePassC agent daemon""" 23 | 24 | def __init__(self, pidfile, loglevel, logfile, 25 | server_address = 'localhost', server_port = 50000, 26 | agent_port = 50001, password = None, keyfile = None, 27 | tls = False, tls_dir = None): 28 | Daemon.__init__(self, pidfile) 29 | 30 | try: 31 | logdir = realpath(expanduser(getenv('XDG_DATA_HOME'))) 32 | except: 33 | logdir = realpath(expanduser('~/.local/share')) 34 | finally: 35 | logfile = join(logdir, 'keepassc', logfile) 36 | 37 | logging.basicConfig(format='[%(levelname)s] in %(filename)s:' 38 | '%(funcName)s at %(asctime)s\n%(message)s', 39 | level=loglevel, filename=logfile, 40 | filemode='a') 41 | 42 | self.lookup = { 43 | b'FIND': self.find, 44 | b'GET': self.get_db, 45 | b'GETC': self.get_credentials} 46 | 47 | self.server_address = (server_address, server_port) 48 | try: 49 | # Listen for commands 50 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 51 | self.sock.bind(("localhost", agent_port)) 52 | self.sock.listen(1) 53 | except OSError as err: 54 | print(err) 55 | logging.error(err.__str__()) 56 | sys.exit(1) 57 | else: 58 | logging.info('Agent socket created on localhost:'+ 59 | str(agent_port)) 60 | 61 | if tls_dir is not None: 62 | self.tls_dir = realpath(expanduser(tls_dir)).encode() 63 | else: 64 | self.tls_dir = b'' 65 | 66 | chdir("/var/empty") 67 | 68 | self.password = password 69 | # Agent is a daemon and cannot find the keyfile after run 70 | if keyfile is not None: 71 | with open(keyfile, "rb") as handler: 72 | self.keyfile = handler.read() 73 | handler.close() 74 | else: 75 | self.keyfile = b'' 76 | 77 | if tls is True: 78 | self.context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) 79 | self.context.verify_mode = ssl.CERT_REQUIRED 80 | self.context.load_verify_locations(tls_dir + "/cacert.pem") 81 | else: 82 | self.context = None 83 | 84 | #Handle SIGTERM 85 | signal.signal(signal.SIGTERM, self.handle_sigterm) 86 | 87 | def send_cmd(self, *cmd): 88 | """Overrides Client.connect_server""" 89 | 90 | if self.password is None: 91 | password = b'' 92 | else: 93 | password = self.password.encode() 94 | 95 | tmp = [password, self.keyfile] 96 | tmp.extend(cmd) 97 | cmd_chain = build_message(tmp) 98 | 99 | try: 100 | tmp_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 101 | if self.context is not None: 102 | conn = self.context.wrap_socket(tmp_conn) 103 | else: 104 | conn = tmp_conn 105 | conn.connect(self.server_address) 106 | except: 107 | raise 108 | else: 109 | logging.info('Connected to '+self.server_address[0]+':'+ 110 | str(self.server_address[1])) 111 | 112 | try: 113 | conn.settimeout(60) 114 | if self.context is not None: 115 | if not isfile(self.tls_dir.decode() + '/pin'): 116 | sha = sha256() 117 | sha.update(conn.getpeercert(True)) 118 | with open(self.tls_dir.decode() + '/pin', 'wb') as pin: 119 | pin.write(sha.digest()) 120 | else: 121 | with open(self.tls_dir.decode() + '/pin', 'rb') as pin: 122 | pinned_key = pin.read() 123 | sha = sha256() 124 | sha.update(conn.getpeercert(True)) 125 | if pinned_key != sha.digest(): 126 | return (b'FAIL: Server certificate differs from ' 127 | b'pinned certificate') 128 | cert = conn.getpeercert() 129 | try: 130 | ssl.match_hostname(cert, "KeePassC Server") 131 | except: 132 | return b'FAIL: TLS - Hostname does not match' 133 | sendmsg(conn, cmd_chain) 134 | answer = receive(conn) 135 | except: 136 | raise 137 | finally: 138 | conn.shutdown(socket.SHUT_RDWR) 139 | conn.close() 140 | 141 | return answer 142 | 143 | def run(self): 144 | """Overide Daemon.run() and provide sockets""" 145 | 146 | while True: 147 | try: 148 | conn, client = self.sock.accept() 149 | except OSError: 150 | break 151 | 152 | logging.info('Connected to '+client[0]+':'+str(client[1])) 153 | conn.settimeout(60) 154 | 155 | try: 156 | parts = receive(conn).split(b'\xB2\xEA\xC0') 157 | cmd = parts.pop(0) 158 | if cmd in self.lookup: 159 | self.lookup[cmd](conn, parts) 160 | else: 161 | logging.error('Received a wrong command') 162 | sendmsg(conn, b'FAIL: Command isn\'t available') 163 | except OSError as err: 164 | logging.error(err.__str__()) 165 | finally: 166 | conn.shutdown(socket.SHUT_RDWR) 167 | conn.close() 168 | 169 | def find(self, conn, cmd_misc): 170 | """Find Entries""" 171 | 172 | try: 173 | answer = self.send_cmd(b'FIND', cmd_misc[0]) 174 | sendmsg(conn, answer) 175 | if answer[:4] == b'FAIL': 176 | raise OSError(answer.decode()) 177 | except (OSError, TypeError) as err: 178 | logging.error(err.__str__()) 179 | 180 | def get_db(self, conn, cmd_misc): 181 | """Get the whole encrypted database from server""" 182 | 183 | try: 184 | answer = self.send_cmd(b'GET') 185 | sendmsg(conn, answer) 186 | if answer[:4] == b'FAIL': 187 | raise OSError(answer.decode()) 188 | except (OSError, TypeError) as err: 189 | logging.error(err.__str__()) 190 | 191 | def get_credentials(self, conn, cmd_misc): 192 | """Send password credentials to client""" 193 | 194 | if self.password is None: 195 | password = b'' 196 | else: 197 | password = self.password.encode() 198 | if self.context: 199 | tls = b'True' 200 | else: 201 | tls = b'False' 202 | 203 | tmp = [password, self.keyfile, self.server_address[0].encode(), 204 | str(self.server_address[1]).encode(), tls, 205 | self.tls_dir] 206 | chain = build_message(tmp) 207 | try: 208 | sendmsg(conn, chain) 209 | except (OSError, TypeError) as err: 210 | logging.error(err.__str__()) 211 | 212 | def handle_sigterm(self, signum, frame): 213 | """Handle SIGTERM""" 214 | 215 | self.sock.shutdown(socket.SHUT_RDWR) 216 | self.sock.close() 217 | del self.keyfile 218 | 219 | -------------------------------------------------------------------------------- /keepassc/client.py: -------------------------------------------------------------------------------- 1 | """This module implements the Client class for KeePassC. 2 | 3 | Classes: 4 | Client(Connection) 5 | """ 6 | 7 | import logging 8 | import socket 9 | import ssl 10 | from os.path import join, expanduser, realpath, isfile 11 | from hashlib import sha256 12 | 13 | from keepassc.conn import * 14 | 15 | class Client(object): 16 | """The KeePassC client""" 17 | 18 | def __init__(self, loglevel, logfile, server_address = 'localhost', 19 | server_port = 50000, password = None, keyfile = None, 20 | tls = False, tls_dir = None): 21 | try: 22 | logdir = realpath(expanduser(getenv('XDG_DATA_HOME'))) 23 | except: 24 | logdir = realpath(expanduser('~/.local/share')) 25 | finally: 26 | logfile = join(logdir, 'keepassc', logfile) 27 | 28 | logging.basicConfig(format='[%(levelname)s] in %(filename)s:' 29 | '%(funcName)s at %(asctime)s\n%(message)s', 30 | level=loglevel, filename=logfile, 31 | filemode='a') 32 | 33 | self.password = password 34 | self.keyfile = keyfile 35 | self.server_address = (server_address, server_port) 36 | 37 | self.tls_dir = tls_dir 38 | 39 | if tls is True: 40 | self.context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) 41 | self.context.verify_mode = ssl.CERT_REQUIRED 42 | logging.error(tls_dir) 43 | self.context.load_verify_locations(tls_dir + "/cacert.pem") 44 | else: 45 | self.context = None 46 | 47 | def send_cmd(self, *cmd): 48 | """Send a command to server 49 | 50 | *cmd are arbitary byte strings 51 | 52 | """ 53 | if self.keyfile is not None: 54 | with open(self.keyfile, 'rb') as keyfile: 55 | key = keyfile.read() 56 | else: 57 | key = b'' 58 | if self.password is None: 59 | password = b'' 60 | else: 61 | password = self.password.encode() 62 | 63 | tmp = [password, key] 64 | tmp.extend(cmd) 65 | cmd_chain = build_message(tmp) 66 | 67 | try: 68 | tmp_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 69 | if self.context is not None: 70 | conn = self.context.wrap_socket(tmp_conn) 71 | else: 72 | conn = tmp_conn 73 | conn.connect(self.server_address) 74 | except: 75 | raise 76 | else: 77 | logging.info('Connected to '+self.server_address[0]+':'+ 78 | str(self.server_address[1])) 79 | try: 80 | conn.settimeout(60) 81 | if self.context is not None: 82 | if not isfile(self.tls_dir + '/pin'): 83 | sha = sha256() 84 | sha.update(conn.getpeercert(True)) 85 | with open(self.tls_dir + '/pin', 'wb') as pin: 86 | pin.write(sha.digest()) 87 | else: 88 | with open(self.tls_dir + '/pin', 'rb') as pin: 89 | pinned_key = pin.read() 90 | sha = sha256() 91 | sha.update(conn.getpeercert(True)) 92 | if pinned_key != sha.digest(): 93 | return (b'FAIL: Server certificate differs from ' 94 | b'pinned certificate') 95 | cert = conn.getpeercert() 96 | try: 97 | ssl.match_hostname(cert, "KeePassC Server") 98 | except: 99 | return b'FAIL: TLS - Hostname does not match' 100 | sendmsg(conn, cmd_chain) 101 | answer = receive(conn) 102 | except: 103 | raise 104 | finally: 105 | conn.shutdown(socket.SHUT_RDWR) 106 | conn.close() 107 | 108 | return answer 109 | 110 | def get_bytes(self, cmd, *misc): 111 | """Send a command and get the answer as bytes 112 | 113 | cmd is a bytestring with the command 114 | *misc are arbitary bytestring needed for the command 115 | 116 | """ 117 | 118 | try: 119 | db_buf = self.send_cmd(cmd, *misc) 120 | if db_buf[:4] == b'FAIL': 121 | raise OSError(db_buf.decode()) 122 | return db_buf 123 | except (OSError, TypeError) as err: 124 | logging.error(err.__str__()) 125 | return err.__str__() 126 | 127 | def get_string(self, cmd, *misc): 128 | """Send a command and get the answer decoded""" 129 | 130 | try: 131 | answer = self.send_cmd(cmd, *misc).decode() 132 | if answer[:4] == b'FAIL': 133 | raise OSError(answer) 134 | return answer 135 | except (OSError, TypeError) as err: 136 | logging.error(err.__str__()) 137 | return err.__str__() 138 | 139 | def find(self, title): 140 | """Find entries by title""" 141 | 142 | return self.get_string(b'FIND', title) 143 | 144 | def get_db(self): 145 | """Just get the whole encrypted database from server""" 146 | 147 | return self.get_bytes(b'GET') 148 | 149 | def change_password(self, password, keyfile): 150 | """Change the password of the remote database 151 | 152 | This is only allowed from localhost (127.0.0.1 153 | 154 | """ 155 | 156 | return self.get_string(b'CHANGESECRET', password, keyfile) 157 | 158 | def create_group(self, title, root): 159 | """Create a group 160 | 161 | 162 | title is the group title, root is the id of the parent group 163 | 164 | """ 165 | 166 | return self.get_bytes(b'NEWG', title, root) 167 | 168 | def create_entry(self, title, url, username, password, comment, y, mon, d, 169 | group_id): 170 | """Create an entry 171 | 172 | Watch the kppy documentation for an explanation of the arguments 173 | 174 | """ 175 | 176 | return self.get_bytes(b'NEWE', title, url, username, password, comment, 177 | y, mon, d, group_id) 178 | 179 | def delete_group(self, group_id, last_mod): 180 | """Delete a group by the id 181 | 182 | last_mod is needed to check if the group was updated since the 183 | last refresh 184 | 185 | """ 186 | 187 | return self.get_bytes(b'DELG', group_id, str(last_mod[0]).encode(), 188 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 189 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 190 | str(last_mod[5]).encode()) 191 | 192 | def delete_entry(self, uuid, last_mod): 193 | """Delete an entry by uuid""" 194 | 195 | return self.get_bytes(b'DELE', uuid, str(last_mod[0]).encode(), 196 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 197 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 198 | str(last_mod[5]).encode()) 199 | 200 | def move_group(self, group_id, root): 201 | """Move a group to a new parent 202 | 203 | If root is 0 the group with id group_id is moved to the root 204 | 205 | """ 206 | 207 | return self.get_bytes(b'MOVG', group_id, root) 208 | 209 | def move_entry(self, uuid, root): 210 | """Move an entry with uuid to the group with id root""" 211 | 212 | return self.get_bytes(b'MOVE', uuid, root) 213 | 214 | def set_g_title(self, title, group_id, last_mod): 215 | """Set the title of a group""" 216 | 217 | return self.get_bytes(b'TITG', title, group_id, 218 | str(last_mod[0]).encode(), 219 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 220 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 221 | str(last_mod[5]).encode()) 222 | 223 | def set_e_title(self, title, uuid, last_mod): 224 | """Set the title of an entry""" 225 | 226 | return self.get_bytes(b'TITE', title, uuid, str(last_mod[0]).encode(), 227 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 228 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 229 | str(last_mod[5]).encode()) 230 | 231 | def set_e_user(self, username, uuid, last_mod): 232 | """Set the username of an entry""" 233 | 234 | return self.get_bytes(b'USER', username, uuid, 235 | str(last_mod[0]).encode(), 236 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 237 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 238 | str(last_mod[5]).encode()) 239 | 240 | def set_e_url(self, url, uuid, last_mod): 241 | """Set the URL of an entry""" 242 | 243 | return self.get_bytes(b'URL', url, uuid, str(last_mod[0]).encode(), 244 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 245 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 246 | str(last_mod[5]).encode()) 247 | 248 | def set_e_comment(self, comment, uuid, last_mod): 249 | """Set the comment of an entry""" 250 | 251 | return self.get_bytes(b'COMM', comment, uuid, 252 | str(last_mod[0]).encode(), 253 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 254 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 255 | str(last_mod[5]).encode()) 256 | 257 | def set_e_pass(self, password, uuid, last_mod): 258 | """Set the password of an entry""" 259 | 260 | return self.get_bytes(b'PASS', password, uuid, 261 | str(last_mod[0]).encode(), 262 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 263 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 264 | str(last_mod[5]).encode()) 265 | 266 | def set_e_exp(self, y, mon, d, uuid, last_mod): 267 | """Set the expiration date of an entry""" 268 | 269 | return self.get_bytes(b'DATE', y, mon, d, uuid, 270 | str(last_mod[0]).encode(), 271 | str(last_mod[1]).encode(), str(last_mod[2]).encode(), 272 | str(last_mod[3]).encode(), str(last_mod[4]).encode(), 273 | str(last_mod[5]).encode()) 274 | 275 | -------------------------------------------------------------------------------- /keepassc/conn.py: -------------------------------------------------------------------------------- 1 | """This module implements some functions for a connection. 2 | 3 | Functions: 4 | build_message(parts) 5 | receive(conn) 6 | sendmsg(sock, msg) 7 | """ 8 | 9 | import logging 10 | 11 | 12 | def build_message(parts): 13 | """Join many parts to one message with a seperator 14 | 15 | A message will look like 16 | 17 | b'foo\xB2\xEA\xC0bar\xB2\xEA\xC0foobar' 18 | 19 | so that it could easily splitted by .split 20 | 21 | parts has to be a tuple of bytestrings 22 | 23 | """ 24 | 25 | msg = b'' 26 | for i in parts[:-1]: 27 | msg += i 28 | msg += b'\xB2\xEA\xC0' # \xB2\xEA\xC0 = BREAK 29 | msg += parts[-1] 30 | 31 | return msg 32 | 33 | def receive(conn): 34 | """Receive a message 35 | 36 | conn has to be the socket which receive the message 37 | 38 | A message has to end with the bytestring b'\xDE\xAD\xE1\x1D' 39 | 40 | """ 41 | 42 | ip, port = conn.getpeername() 43 | logging.info('Receiving a message from '+ip+':'+str(port)) 44 | data = b'' 45 | while True: 46 | try: 47 | received = conn.recv(16) 48 | if not received: 49 | logging.error("No data received") 50 | break 51 | except: 52 | raise 53 | if b'\xDE\xAD\xE1\x1D' in received: 54 | data += received[:received.find(b'\xDE\xAD\xE1\x1D')] 55 | break 56 | else: 57 | data += received 58 | if data[-4:] == b'\xDE\xAD\xE1\x1D': 59 | data = data[:-4] 60 | break 61 | return data 62 | 63 | def sendmsg(sock, msg): 64 | """Send message 65 | 66 | sock is the socket which sends the message 67 | 68 | msg hast to be a bytestring 69 | 70 | """ 71 | 72 | ip, port = sock.getpeername() 73 | try: 74 | logging.info('Send a message to '+ip+':'+str(port)) 75 | # \xDE\xAD\xE1\x1D = DEAD END 76 | sock.sendall(msg + b'\xDE\xAD\xE1\x1D') 77 | except: 78 | raise 79 | 80 | -------------------------------------------------------------------------------- /keepassc/control.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import curses as cur 4 | import logging 5 | from curses.ascii import NL, DEL, SP 6 | from datetime import date, datetime 7 | from os import chdir, getcwd, getenv, geteuid, makedirs, remove 8 | from os.path import expanduser, isfile, isdir, realpath, join 9 | from pwd import getpwuid 10 | from random import sample 11 | from socket import gethostname, socket, AF_INET, SOCK_STREAM, SHUT_RDWR 12 | from sys import exit 13 | 14 | from kppy.database import KPDBv1 15 | from kppy.exceptions import KPError 16 | 17 | from keepassc.conn import * 18 | from keepassc.client import Client 19 | from keepassc.editor import Editor 20 | from keepassc.helper import parse_config, write_config 21 | from keepassc.filebrowser import FileBrowser 22 | from keepassc.dbbrowser import DBBrowser 23 | 24 | 25 | class Control(object): 26 | '''This class represents the whole application.''' 27 | def __init__(self): 28 | '''The __init__-method. 29 | 30 | It just initializes some variables and settings and changes 31 | the working directory to /var/empty to prevent coredumps as 32 | normal user. 33 | 34 | ''' 35 | 36 | try: 37 | self.config_home = realpath(expanduser(getenv('XDG_CONFIG_HOME'))) 38 | except: 39 | self.config_home = realpath(expanduser('~/.config')) 40 | finally: 41 | self.config_home = join(self.config_home, 'keepassc', 'config') 42 | 43 | try: 44 | self.data_home = realpath(expanduser(getenv('XDG_DATA_HOME'))) 45 | except: 46 | self.data_home = realpath(expanduser('~/.local/share/')) 47 | finally: 48 | self.data_home = join(self.data_home, 'keepassc') 49 | self.last_home = join(self.data_home, 'last') 50 | self.remote_home = join(self.data_home, 'remote') 51 | self.key_home = join(self.data_home, 'key') 52 | 53 | self.config = parse_config(self) 54 | 55 | if self.config['rem_key'] is False and isfile(self.key_home): 56 | remove(self.key_home) 57 | 58 | self.initialize_cur() 59 | self.last_file = None 60 | self.last_key = None 61 | self.loginname = getpwuid(geteuid())[0] 62 | self.hostname = gethostname() 63 | self.cur_dir = getcwd() 64 | chdir('/var/empty') 65 | self.db = None 66 | 67 | def initialize_cur(self): 68 | '''Method to initialize curses functionality''' 69 | 70 | self.stdscr = cur.initscr() 71 | try: 72 | cur.curs_set(0) 73 | except: 74 | print('Invisible cursor not supported') 75 | cur.cbreak() 76 | cur.noecho() 77 | self.stdscr.keypad(1) 78 | cur.start_color() 79 | cur.use_default_colors() 80 | cur.init_pair(1, -1, -1) 81 | cur.init_pair(2, 2, -1) 82 | cur.init_pair(3, -1, 1) 83 | cur.init_pair(4, 6, -1) 84 | cur.init_pair(5, 0, 6) 85 | cur.init_pair(6, 0, 7) 86 | cur.init_pair(7, 1, -1) 87 | self.stdscr.bkgd(1) 88 | self.ysize, self.xsize = self.stdscr.getmaxyx() 89 | 90 | self.group_win = cur.newwin(self.ysize - 1, int(self.xsize / 3), 91 | 1, 0) 92 | # 11 is the y size of info_win 93 | self.entry_win = cur.newwin((self.ysize - 1) - 11, 94 | int(2 * self.xsize / 3), 95 | 1, int(self.xsize / 3)) 96 | self.info_win = cur.newwin(11, 97 | int(2 * self.xsize / 3), 98 | (self.ysize - 1) - 11, 99 | int(self.xsize / 3)) 100 | self.group_win.keypad(1) 101 | self.entry_win.keypad(1) 102 | self.group_win.bkgd(1) 103 | self.entry_win.bkgd(1) 104 | self.info_win.bkgd(1) 105 | 106 | def resize_all(self): 107 | '''Method to resize windows''' 108 | 109 | self.ysize, self.xsize = self.stdscr.getmaxyx() 110 | self.group_win.resize(self.ysize - 1, int(self.xsize / 3)) 111 | self.entry_win.resize( 112 | self.ysize - 1 - 11, int(2 * self.xsize / 3)) 113 | self.info_win.resize(11, int(2 * self.xsize / 3)) 114 | self.group_win.mvwin(1, 0) 115 | self.entry_win.mvwin(1, int(self.xsize / 3)) 116 | self.info_win.mvwin((self.ysize - 1) - 11, int(self.xsize / 3)) 117 | 118 | def any_key(self): 119 | '''If any key is needed.''' 120 | 121 | while True: 122 | try: 123 | e = self.stdscr.getch() 124 | except KeyboardInterrupt: 125 | e = 4 126 | if e == 4: 127 | return -1 128 | elif e == cur.KEY_RESIZE: 129 | self.resize_all() 130 | else: 131 | return e 132 | 133 | def draw_text(self, changed, *misc): 134 | '''This method is a wrapper to display some text on stdscr. 135 | 136 | misc is a list that should consist of 3-tuples which holds 137 | text to display. 138 | (1st element: y-coordinate, 2nd: x-coordinate, 3rd: text) 139 | 140 | ''' 141 | 142 | if changed is True: 143 | cur_dir = self.cur_dir + '*' 144 | else: 145 | cur_dir = self.cur_dir 146 | try: 147 | self.stdscr.clear() 148 | self.stdscr.addstr( 149 | 0, 0, self.loginname + '@' + self.hostname + ':', 150 | cur.color_pair(2)) 151 | self.stdscr.addstr( 152 | 0, len(self.loginname + '@' + self.hostname + ':'), 153 | cur_dir) 154 | for i, j, k in misc: 155 | self.stdscr.addstr(i, j, k) 156 | except: # to prevent a crash if screen is small 157 | pass 158 | finally: 159 | self.stdscr.refresh() 160 | 161 | def draw_help(self, *text): 162 | """Draw a help 163 | 164 | *text are arbitary string 165 | 166 | """ 167 | 168 | if len(text) > self.ysize -1: 169 | length = self.ysize - 1 170 | offset = 0 171 | spill = len(text) - self.ysize + 2 172 | else: 173 | length = len(text) 174 | offset = 0 175 | spill = 0 176 | 177 | while True: 178 | try: 179 | self.draw_text(False) 180 | for i in range(length): 181 | self.stdscr.addstr( 182 | i + 1, 0, text[(i + offset)]) 183 | except: 184 | pass 185 | finally: 186 | self.stdscr.refresh() 187 | try: 188 | e = self.stdscr.getch() 189 | except KeyboardInterrupt: 190 | e = 4 191 | 192 | if e == cur.KEY_DOWN: 193 | if offset < spill: 194 | offset += 1 195 | elif e == cur.KEY_UP: 196 | if offset > 0: 197 | offset -= 1 198 | elif e == NL: 199 | return 200 | elif e == cur.KEY_RESIZE: 201 | self.resize_all() 202 | if len(text) > self.ysize -1: 203 | length = self.ysize - 1 204 | offset = 0 205 | spill = len(text) - self.ysize + 2 206 | else: 207 | length = len(text) 208 | offset = 0 209 | spill = 0 210 | elif e == 4: 211 | if self.db is not None: 212 | self.db.close() 213 | self.close() 214 | 215 | def get_password(self, std, needed=True): 216 | '''This method is used to get a password. 217 | 218 | The pasword will not be displayed during typing. 219 | 220 | std is a string that should be displayed. If needed is True it 221 | is not possible to return an emptry string. 222 | 223 | ''' 224 | password = Editor(self.stdscr, max_text_size=1, win_location=(0, 1), 225 | win_size=(1, self.xsize), title=std, pw_mode=True)() 226 | if needed is True and not password: 227 | return False 228 | else: 229 | return password 230 | 231 | def get_authentication(self): 232 | """Get authentication credentials""" 233 | 234 | while True: 235 | if (self.config['skip_menu'] is False or 236 | (self.config['rem_db'] is False and 237 | self.config['rem_key'] is False)): 238 | auth = self.gen_menu(1, ( 239 | (1, 0, 'Use a password (1)'), 240 | (2, 0, 'Use a keyfile (2)'), 241 | (3, 0, 'Use both (3)')), 242 | (5, 0, 'Press \'F5\' to go back to main ' 243 | 'menu')) 244 | else: 245 | self.draw_text(False) 246 | auth = 3 247 | if auth is False: 248 | return False 249 | elif auth == -1: 250 | self.close() 251 | if auth == 1 or auth == 3: 252 | if self.config['skip_menu'] is True: 253 | needed = False 254 | else: 255 | needed = True 256 | password = self.get_password('Password: ', needed = needed) 257 | if password is False: 258 | self.config['skip_menu'] = False 259 | continue 260 | elif password == -1: 261 | self.close() 262 | # happens only if self.config['skip_menu'] is True 263 | elif password == "": 264 | password = None 265 | if auth != 3: 266 | keyfile = None 267 | if auth == 2 or auth == 3: 268 | # Ugly construct but works 269 | # "if keyfile is False" stuff is needed to implement the 270 | # return to previous screen stuff 271 | # Use similar constructs elsewhere 272 | while True: 273 | self.get_last_key() 274 | if (self.last_key is None or 275 | self.config['rem_key'] is False): 276 | ask_for_lf = False 277 | else: 278 | ask_for_lf = True 279 | 280 | keyfile = FileBrowser(self, ask_for_lf, True, 281 | self.last_key)() 282 | if keyfile is False: 283 | break 284 | elif keyfile == -1: 285 | self.close() 286 | elif not isfile(keyfile): 287 | self.draw_text(False, 288 | (1, 0, 'That\'s not a file'), 289 | (3, 0, 'Press any key.')) 290 | if self.any_key() == -1: 291 | self.close() 292 | continue 293 | break 294 | if keyfile is False: 295 | continue 296 | if auth != 3: 297 | password = None 298 | if self.config['rem_key'] is True: 299 | if not isdir(self.key_home[:-4]): 300 | if isfile(self.key_home[:-4]): 301 | remove(self.key_home[:-4]) 302 | makedirs(self.key_home[:-4]) 303 | handler = open(self.key_home, 'w') 304 | handler.write(keyfile) 305 | handler.close() 306 | break 307 | return (password, keyfile) 308 | 309 | def get_last_db(self): 310 | if isfile(self.last_home) and self.config['rem_db'] is False: 311 | remove(self.last_home) 312 | self.last_file = None 313 | elif isfile(self.last_home): 314 | try: 315 | handler = open(self.last_home, 'r') 316 | except Exception as err: 317 | self.last_file = None 318 | print(err.__str__()) 319 | else: 320 | self.last_file = handler.readline() 321 | handler.close() 322 | else: 323 | self.last_file = None 324 | 325 | def get_last_key(self): 326 | if isfile(self.key_home) and self.config['rem_key'] is False: 327 | remove(self.key_home) 328 | self.last_key = None 329 | elif isfile(self.key_home): 330 | try: 331 | handler = open(self.key_home, 'r') 332 | except Exception as err: 333 | self.last_key = None 334 | print(err.__str__()) 335 | else: 336 | self.last_key = handler.readline() 337 | handler.close() 338 | else: 339 | self.last_key = None 340 | 341 | def gen_pass(self): 342 | '''Method to generate a password''' 343 | 344 | while True: 345 | items = self.gen_check_menu(((1, 0, 'Include numbers'), 346 | (2, 0, 347 | 'Include capitalized letters'), 348 | (3, 0, 'Include special symbols'), 349 | (4, 0, 'Include space')), 350 | (6, 0, 'Press space to un-/check'), 351 | (7, 0, 352 | 'Press return to enter options')) 353 | if items is False or items == -1: 354 | return items 355 | length = self.get_num('Password length: ') 356 | if length is False: 357 | continue 358 | elif length == -1: 359 | return -1 360 | char_set = 'abcdefghijklmnopqrstuvwxyz' 361 | if items[0] == 1: 362 | char_set += '1234567890' 363 | if items[1] == 1: 364 | char_set += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 365 | if items[2] == 1: 366 | char_set += '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~$' 367 | if items[3] == 1: 368 | char_set += ' ' 369 | 370 | password = '' 371 | for _ in range(length): 372 | password += sample(char_set, 1)[0] 373 | return password 374 | 375 | def get_exp_date(self, *exp): 376 | nav = self.gen_menu(1, 377 | ((1, 0, 'Expires never (1)'), 378 | (2, 0, 'Set expire date manual (2)'), 379 | (3, 0, 'Expires tomorrow (3)'), 380 | (4, 0, 'Expires in 1 week (4)'), 381 | (5, 0, 'Expires in 2 weeks (5)'), 382 | (6, 0, 'Expires in 1 month (6)'), 383 | (7, 0, 'Expires in 3 months (7)'), 384 | (8, 0, 'Expires in 6 months (8)'), 385 | (9, 0, 'Expires in 1 year (9)'))) 386 | 387 | tmp_date = datetime.now() 388 | d = tmp_date.day 389 | m = tmp_date.month 390 | y = tmp_date.year 391 | if nav == 1: 392 | exp_date = (2999, 12, 28) 393 | elif nav == 2: 394 | if exp: 395 | exp_date = self.get_manual_exp_date(exp[0], exp[1], exp[2]) 396 | else: 397 | exp_date = self.get_manual_exp_date() 398 | elif nav == 3: 399 | if (((m == 1 or m == 3 or m == 5 or m == 7 or m == 8 or m == 10) and d == 31) or 400 | ((m == 4 or m == 6 or m == 9 or m == 11) and d == 30)): 401 | exp_date = (y, m + 1, 1) 402 | elif (m == 2 and ((y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) and d == 29) or d == 28)): 403 | exp_date = (y, 3, 1) 404 | elif m == 12 and d == 31: 405 | exp_date = (y + 1, 1, 1) 406 | else: 407 | exp_date = (y, m, d + 1) 408 | elif nav == 4: 409 | if ((m == 1 or m == 3 or m == 5 or m == 7 or m == 8 or m == 10) and d + 7 > 31): 410 | exp_date = (y, m + 1, (d + 7) % 31) 411 | elif ((m == 4 or m == 6 or m == 9 or m == 11) and d + 7 > 30): 412 | exp_date = (y, m + 1, (d + 7) % 30) 413 | elif (m == 2 and (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) and d + 7 > 29)): 414 | exp_date = (y, 3, (d + 7) % 29) 415 | elif (m == 2 and d + 7 > 28): 416 | exp_date = (y, 3, (d + 7) % 28) 417 | elif m == 12 and d + 7 > 31: 418 | exp_date = (y + 1, 1, (d + 7) % 31) 419 | else: 420 | exp_date = (y, m, d + 7) 421 | elif nav == 5: 422 | if ((m == 1 or m == 3 or m == 5 or m == 7 or m == 8 or m == 10) and d + 14 > 31): 423 | exp_date = (y, m + 1, (d + 14) % 31) 424 | elif ((m == 4 or m == 6 or m == 9 or m == 11) and d + 14 > 30): 425 | exp_date = (y, m + 1, (d + 14) % 30) 426 | elif (m == 2 and (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) and d + 14 > 29)): 427 | exp_date = (y, 3, (d + 14) % 29) 428 | elif (m == 2 and d + 14 > 28): 429 | exp_date = (y, 3, (d + 14) % 28) 430 | elif m == 12 and d + 14 > 31: 431 | exp_date = (y + 1, 1, (d + 14) % 31) 432 | else: 433 | exp_date = (y, m, d + 14) 434 | elif nav == 6: 435 | if m == 1 and (y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) and d > 29): 436 | exp_date = (y, 3, d % 29) 437 | elif (m == 1 and d > 28): 438 | exp_date = (y, 3, d % 28) 439 | elif (m == 3 or m == 5 or m == 7 or m == 8 or m == 10) and d == 31: 440 | exp_date = (y, m + 2, 1) 441 | elif m == 12: 442 | exp_date = (y + 1, 1, d) 443 | else: 444 | exp_date = (y, m + 1, d) 445 | elif nav == 7: 446 | if (m == 1 or m == 3 or m == 8) and d == 31: 447 | exp_date = (y, m + 4, 1) 448 | elif m == 10 or m == 12: 449 | exp_date = (y + 1, (m + 3) % 12, d) 450 | elif m == 11 and ((y + 1) % 4 == 0 and ((y + 1) % 100 != 0 or (y + 1) % 400 == 0)) and d > 29: 451 | exp_date = (y + 1, 3, d % 29) 452 | elif m == 11 and d > 28: 453 | exp_date = (y + 1, 3, d % 28) 454 | else: 455 | exp_date = (y, m + 3, d) 456 | elif nav == 8: 457 | if (m == 3 or m == 5) and d == 31: 458 | exp_date = (y, m + 7, 1) 459 | elif (m == 7 or m == 9 or m == 11): 460 | exp_date = (y + 1, (m + 6) % 12, d) 461 | elif m == 8 and ((y + 1) % 4 == 0 and ((y + 1) % 100 != 0 or (y + 1) % 400 == 0)) and d > 29: 462 | exp_date = (y + 1, 3, d % 29) 463 | elif m == 8 and d > 28: 464 | exp_date = (y + 1, 3, d % 28) 465 | elif (m == 10 or m == 12) and d == 31: 466 | exp_date = (y + 1, (m + 7) % 12, 1) 467 | elif (m == 8 or m == 10 or m == 12): 468 | exp_date = (y + 1, (m + 6) % 12, d) 469 | else: 470 | exp_date = (y, m + 6, d) 471 | elif nav == 9: 472 | if m == 2 and d == 29: 473 | exp_date = (y + 1, 3, 1) 474 | else: 475 | exp_date = (y + 1, m, d) 476 | elif nav == -1: 477 | return -1 478 | elif nav is False: 479 | return False 480 | 481 | return exp_date 482 | 483 | def get_manual_exp_date(self, *exp): 484 | '''This method is used to get an expiration date for entries. 485 | 486 | exp is used to display an actual expiration date. 487 | 488 | ''' 489 | 490 | pass_y = False 491 | pass_mon = False 492 | goto_last = False 493 | while True: 494 | if pass_y is False: 495 | edit = '' 496 | e = cur.KEY_BACKSPACE 497 | while e != NL: 498 | if (e == cur.KEY_BACKSPACE or e == DEL) and len(edit) != 0: 499 | edit = edit[:-1] 500 | elif e == cur.KEY_BACKSPACE or e == DEL: 501 | pass 502 | elif e == 4: 503 | return -1 504 | elif e == cur.KEY_RESIZE: 505 | self.resize_all() 506 | elif e == cur.KEY_F5: 507 | return False 508 | elif len(edit) < 4 and e >= 48 and e <= 57: 509 | edit += chr(e) 510 | self.draw_text(False, 511 | (1, 0, 'Special date 2999-12-28 means that ' 512 | 'the expires never.'), 513 | (3, 0, 'Year: ' + edit)) 514 | if exp: 515 | try: 516 | self.stdscr.addstr(2, 0, 517 | 'Actual expiration date: ' + 518 | str(exp[0]) + '-' + 519 | str(exp[1]) + '-' + 520 | str(exp[2])) 521 | except: 522 | pass 523 | finally: 524 | self.stdscr.refresh() 525 | try: 526 | e = self.stdscr.getch() 527 | except KeyboardInterrupt: 528 | e = 4 529 | if e == NL and edit == '': 530 | e = cur.KEY_BACKSPACE 531 | continue 532 | y = int(edit) 533 | pass_y = True 534 | 535 | if pass_mon is False: 536 | edit = '' 537 | e = cur.KEY_BACKSPACE 538 | while e != NL: 539 | if (e == cur.KEY_BACKSPACE or e == DEL) and len(edit) != 0: 540 | edit = edit[:-1] 541 | elif e == cur.KEY_BACKSPACE or e == DEL: 542 | pass 543 | elif e == 4: 544 | return -1 545 | elif e == cur.KEY_RESIZE: 546 | self.resize_all() 547 | elif e == cur.KEY_F5: 548 | pass_y = False 549 | goto_last = True 550 | break 551 | elif len(edit) < 2 and e >= 48 and e <= 57: 552 | edit += chr(e) 553 | self.draw_text(False, 554 | (1, 0, 'Special date 2999-12-28 means that ' 555 | 'the expires never.'), 556 | (3, 0, 'Year: ' + str(y)), 557 | (4, 0, 'Month: ' + edit)) 558 | if exp: 559 | try: 560 | self.stdscr.addstr(2, 0, 561 | 'Actual expiration date: ' + 562 | str(exp[0]) + '-' + 563 | str(exp[1]) + '-' + 564 | str(exp[2])) 565 | except: 566 | pass 567 | finally: 568 | self.stdscr.refresh() 569 | try: 570 | e = self.stdscr.getch() 571 | except KeyboardInterrupt: 572 | e = 4 573 | 574 | if e == NL and edit == '': 575 | e = cur.KEY_BACKSPACE 576 | continue 577 | elif e == NL and (int(edit) > 12 or int(edit) < 1): 578 | self.draw_text(False, 579 | (1, 0, 580 | 'Month must be between 1 and 12. ' 581 | 'Press any key.')) 582 | if self.any_key() == -1: 583 | return -1 584 | e = '' 585 | if goto_last is True: 586 | goto_last = False 587 | continue 588 | mon = int(edit) 589 | pass_mon = True 590 | 591 | edit = '' 592 | e = cur.KEY_BACKSPACE 593 | while e != NL: 594 | if (e == cur.KEY_BACKSPACE or e == DEL) and len(edit) != 0: 595 | edit = edit[:-1] 596 | elif e == cur.KEY_BACKSPACE or e == DEL: 597 | pass 598 | elif e == 4: 599 | return -1 600 | elif e == cur.KEY_RESIZE: 601 | self.resize_all() 602 | elif e == cur.KEY_F5: 603 | pass_mon = False 604 | goto_last = True 605 | break 606 | elif len(edit) < 2 and e >= 48 and e <= 57: 607 | edit += chr(e) 608 | self.draw_text(False, 609 | (1, 0, 'Special date 2999-12-28 means that the ' 610 | 'expires never.'), 611 | (3, 0, 'Year: ' + str(y)), 612 | (4, 0, 'Month: ' + str(mon)), 613 | (5, 0, 'Day: ' + edit)) 614 | if exp: 615 | try: 616 | self.stdscr.addstr(2, 0, 'Actual expiration date: ' + 617 | str(exp[0]) + '-' + 618 | str(exp[1]) + '-' + 619 | str(exp[2])) 620 | except: 621 | pass 622 | finally: 623 | self.stdscr.refresh() 624 | try: 625 | e = self.stdscr.getch() 626 | except KeyboardInterrupt: 627 | e = 4 628 | 629 | if e == NL and edit == '': 630 | e = cur.KEY_BACKSPACE 631 | continue 632 | elif (e == NL and (mon == 1 or mon == 3 or mon == 5 or 633 | mon == 7 or mon == 8 or mon == 10 or 634 | mon == 12) and 635 | (int(edit) > 31 or int(edit) < 0)): 636 | self.draw_text(False, 637 | (1, 0, 638 | 'Day must be between 1 and 31. Press ' 639 | 'any key.')) 640 | if self.any_key() == -1: 641 | return -1 642 | e = '' 643 | elif (e == NL and mon == 2 and (int(edit) > 28 or 644 | int(edit) < 0)): 645 | self.draw_text(False, 646 | (1, 0, 647 | 'Day must be between 1 and 28. Press ' 648 | 'any key.')) 649 | if self.any_key() == -1: 650 | return -1 651 | e = '' 652 | elif (e == NL and (mon == 4 or mon == 6 or mon == 9 or 653 | mon == 11) and (int(edit) > 30 or int(edit) < 0)): 654 | self.draw_text(False, 655 | (1, 0, 656 | 'Day must be between 1 and 30. Press ' 657 | 'any key.')) 658 | if self.any_key() == -1: 659 | return -1 660 | e = '' 661 | if goto_last is True: 662 | goto_last = False 663 | pass_mon = False 664 | continue 665 | d = int(edit) 666 | break 667 | return (y, mon, d) 668 | 669 | def get_num(self, std='', edit='', length=4): 670 | '''Method to get a number''' 671 | 672 | edit = edit 673 | e = 60 # just an unrecognized letter 674 | while e != NL: 675 | if (e == cur.KEY_BACKSPACE or e == DEL) and len(edit) != 0: 676 | edit = edit[:-1] 677 | elif e == cur.KEY_BACKSPACE or e == DEL: 678 | pass 679 | elif e == 4: 680 | return -1 681 | elif e == cur.KEY_RESIZE: 682 | self.resize_all() 683 | elif e == cur.KEY_F5: 684 | return False 685 | elif len(edit) < length and e >= 48 and e <= 57: 686 | edit += chr(e) 687 | self.draw_text(False, 688 | (1, 0, std + edit)) 689 | try: 690 | e = self.stdscr.getch() 691 | except KeyboardInterrupt: 692 | e = 4 693 | if e == NL and edit == '': 694 | e = cur.KEY_BACKSPACE 695 | continue 696 | return int(edit) 697 | 698 | def gen_menu(self, highlight, misc, *add): 699 | '''A universal method to generate a menu. 700 | 701 | misc is a tupel of triples (y, x, 'text') 702 | 703 | add are more tuples but the content should not be accessable 704 | 705 | ''' 706 | 707 | if len(misc) == 0: 708 | return False 709 | h_color = 6 710 | n_color = 1 711 | e = '' 712 | while e != NL: 713 | try: 714 | self.stdscr.clear() 715 | self.stdscr.addstr( 716 | 0, 0, self.loginname + '@' + self.hostname + ':', 717 | cur.color_pair(2)) 718 | self.stdscr.addstr(0, 719 | len(self.loginname + 720 | '@' + self.hostname + ':'), 721 | self.cur_dir) 722 | for i, j, k in misc: 723 | if i == highlight: 724 | self.stdscr.addstr(i, j, k, cur.color_pair(h_color)) 725 | else: 726 | self.stdscr.addstr(i, j, k, cur.color_pair(n_color)) 727 | for i, j, k in add: 728 | self.stdscr.addstr(i, j, k) 729 | except: 730 | pass 731 | finally: 732 | self.stdscr.refresh() 733 | try: 734 | e = self.stdscr.getch() 735 | except KeyboardInterrupt: 736 | e = 4 737 | if e == 4: 738 | return -1 739 | elif e == cur.KEY_RESIZE: 740 | self.resize_all() 741 | elif e == cur.KEY_F5: 742 | return False 743 | elif e == NL: 744 | return highlight 745 | elif (e == cur.KEY_DOWN or e == ord('j')) and highlight < len(misc): 746 | highlight += 1 747 | elif (e == cur.KEY_UP or e == ord('k')) and highlight > 1: 748 | highlight -= 1 749 | elif 49 <= e <= 48 + len(misc): # ASCII(49) = 1 ... 750 | return e - 48 751 | 752 | def gen_check_menu(self, misc, *add): 753 | '''Print a menu with checkable entries''' 754 | 755 | if len(misc) == 0: 756 | return False 757 | items = [] 758 | for i in range(len(misc)): 759 | items.append(0) 760 | highlight = 1 761 | h_color = 6 762 | n_color = 1 763 | e = '' 764 | while e != NL: 765 | try: 766 | self.stdscr.clear() 767 | self.stdscr.addstr( 768 | 0, 0, self.loginname + '@' + self.hostname + ':', 769 | cur.color_pair(2)) 770 | self.stdscr.addstr(0, 771 | len(self.loginname + 772 | '@' + self.hostname + ':'), 773 | self.cur_dir) 774 | for i, j, k in misc: 775 | if items[i - 1] == 0: 776 | check = '[ ]' 777 | else: 778 | check = '[X]' 779 | if i == highlight: 780 | self.stdscr.addstr( 781 | i, j, check + k, cur.color_pair(h_color)) 782 | else: 783 | self.stdscr.addstr( 784 | i, j, check + k, cur.color_pair(n_color)) 785 | for i, j, k in add: 786 | self.stdscr.addstr(i, j, k) 787 | except: 788 | pass 789 | finally: 790 | self.stdscr.refresh() 791 | try: 792 | e = self.stdscr.getch() 793 | except KeyboardInterrupt: 794 | e = 4 795 | if e == 4: 796 | return -1 797 | elif e == cur.KEY_RESIZE: 798 | self.resize_all() 799 | elif e == cur.KEY_F5: 800 | return False 801 | elif e == SP: 802 | if items[highlight - 1] == 0: 803 | items[highlight - 1] = 1 804 | else: 805 | items[highlight - 1] = 0 806 | elif (e == cur.KEY_DOWN or e == ord('j')) and highlight < len(misc): 807 | highlight += 1 808 | elif (e == cur.KEY_UP or e == ord('k')) and highlight > 1: 809 | highlight -= 1 810 | elif e == NL: 811 | return items 812 | 813 | def gen_config_menu(self): 814 | '''The configuration menu''' 815 | 816 | self.config = parse_config(self) 817 | menu = 1 818 | while True: 819 | menu = self.gen_menu(menu, 820 | ((1, 0, 'Delete clipboard automatically: ' + 821 | str(self.config['del_clip'])), 822 | (2, 0, 'Waiting time (seconds): ' + 823 | str(self.config['clip_delay'])), 824 | (3, 0, 'Lock database automatically: ' + 825 | str(self.config['lock_db'])), 826 | (4, 0, 'Waiting time (seconds): ' + 827 | str(self.config['lock_delay'])), 828 | (5, 0, 'Remember last database: ' + 829 | str(self.config['rem_db'])), 830 | (6, 0, 'Remember last keyfile: ' + 831 | str(self.config['rem_key'])), 832 | (7, 0, 'Use directly password and key if one of the two ' 833 | 'above is True: ' + 834 | str(self.config['skip_menu'])), 835 | (8, 0, 'Pin server certificate: ' + str(self.config['pin'])), 836 | (9, 0, 'Generate default configuration'), 837 | (10, 0, 'Write config')), 838 | (12, 0, 'Automatic locking works only for saved databases!')) 839 | if menu == 1: 840 | if self.config['del_clip'] is True: 841 | self.config['del_clip'] = False 842 | elif self.config['del_clip'] is False: 843 | self.config['del_clip'] = True 844 | elif menu == 2: 845 | delay = self.get_num('Waiting time: ', 846 | str(self.config['clip_delay'])) 847 | if delay is False: 848 | continue 849 | elif delay == -1: 850 | self.close() 851 | else: 852 | self.config['clip_delay'] = delay 853 | elif menu == 3: 854 | if self.config['lock_db'] is True: 855 | self.config['lock_db'] = False 856 | elif self.config['lock_db'] is False: 857 | self.config['lock_db'] = True 858 | elif menu == 4: 859 | delay = self.get_num('Waiting time: ', 860 | str(self.config['lock_delay'])) 861 | if delay is False: 862 | continue 863 | elif delay == -1: 864 | self.close() 865 | else: 866 | self.config['lock_delay'] = delay 867 | elif menu == 5: 868 | if self.config['rem_db'] is True: 869 | self.config['rem_db'] = False 870 | elif self.config['rem_db'] is False: 871 | self.config['rem_db'] = True 872 | elif menu == 6: 873 | if self.config['rem_key'] is True: 874 | self.config['rem_key'] = False 875 | elif self.config['rem_key'] is False: 876 | self.config['rem_key'] = True 877 | elif menu == 7: 878 | if self.config['skip_menu'] is True: 879 | self.config['skip_menu'] = False 880 | elif self.config['skip_menu'] is False: 881 | self.config['skip_menu'] = True 882 | elif menu == 8: 883 | if self.config['pin'] is True: 884 | self.config['pin'] = False 885 | elif self.config['pin'] is False: 886 | self.config['pin'] = True 887 | elif menu == 9: 888 | self.config = {'del_clip': True, # standard config 889 | 'clip_delay': 20, 890 | 'lock_db': True, 891 | 'lock_delay': 60, 892 | 'rem_db': True, 893 | 'rem_key': False, 894 | 'skip_menu': False, 895 | 'pin': True} 896 | elif menu == 10: 897 | write_config(self, self.config) 898 | return True 899 | elif menu is False: 900 | return False 901 | elif menu == -1: 902 | self.close() 903 | 904 | def draw_lock_menu(self, changed, highlight, *misc): 905 | '''Draw menu for locked database''' 906 | 907 | h_color = 6 908 | n_color = 1 909 | if changed is True: 910 | cur_dir = self.cur_dir + '*' 911 | else: 912 | cur_dir = self.cur_dir 913 | try: 914 | self.stdscr.clear() 915 | self.stdscr.addstr( 916 | 0, 0, self.loginname + '@' + self.hostname + ':', 917 | cur.color_pair(2)) 918 | self.stdscr.addstr( 919 | 0, len(self.loginname + '@' + self.hostname + ':'), 920 | cur_dir) 921 | for i, j, k in misc: 922 | if i == highlight: 923 | self.stdscr.addstr(i, j, k, cur.color_pair(h_color)) 924 | else: 925 | self.stdscr.addstr(i, j, k, cur.color_pair(n_color)) 926 | #except: # to prevent a crash if screen is small 927 | # pass 928 | finally: 929 | self.stdscr.refresh() 930 | 931 | def main_loop(self, kdb_file=None, remote = False): 932 | '''The main loop. The program alway return to this method.''' 933 | 934 | if remote is True: 935 | self.remote_interface() 936 | else: 937 | # This is needed to remember last database and open it directly 938 | self.get_last_db() 939 | 940 | if kdb_file is not None: 941 | self.cur_dir = kdb_file 942 | if self.open_db(True) is True: 943 | db = DBBrowser(self) 944 | del db 945 | last = self.cur_dir.split('/')[-1] 946 | self.cur_dir = self.cur_dir[:-len(last) - 1] 947 | elif self.last_file is not None and self.config['rem_db'] is True: 948 | self.cur_dir = self.last_file 949 | if self.open_db(True) is True: 950 | db = DBBrowser(self) 951 | del db 952 | last = self.cur_dir.split('/')[-1] 953 | self.cur_dir = self.cur_dir[:-len(last) - 1] 954 | 955 | while True: 956 | self.get_last_db() 957 | menu = self.gen_menu(1, ((1, 0, 'Open existing database (1)'), 958 | (2, 0, 'Create new database (2)'), 959 | (3, 0, 'Connect to a remote database(3)'), 960 | (4, 0, 'Configuration (4)'), 961 | (5, 0, 'Quit (5)')), 962 | (7, 0, 'Type \'F1\' for help inside the editor, ' 963 | 'file or database browser.'), 964 | (8, 0, 'Type \'F5\' to return to the previous' 965 | ' dialog at any time.')) 966 | if menu == 1: 967 | if self.open_db() is False: 968 | continue 969 | db = DBBrowser(self) 970 | del db 971 | last = self.cur_dir.split('/')[-1] 972 | self.cur_dir = self.cur_dir[:-len(last) - 1] 973 | elif menu == 2: 974 | while True: 975 | auth = self.gen_menu(1, ( 976 | (1, 0, 'Use a password (1)'), 977 | (2, 0, 'Use a keyfile (2)'), 978 | (3, 0, 'Use both (3)'))) 979 | password = None 980 | confirm = None 981 | filepath = None 982 | self.db = KPDBv1(new=True) 983 | if auth is False: 984 | break 985 | elif auth == -1: 986 | self.db = None 987 | self.close() 988 | if auth == 1 or auth == 3: 989 | while True: 990 | password = self.get_password('Password: ') 991 | if password is False: 992 | break 993 | elif password == -1: 994 | self.db = None 995 | self.close() 996 | confirm = self.get_password('Confirm: ') 997 | if confirm is False: 998 | break 999 | elif confirm == -1: 1000 | self.db = None 1001 | self.close() 1002 | if password == confirm: 1003 | self.db.password = password 1004 | break 1005 | else: 1006 | self.draw_text(False, 1007 | (1, 0, 1008 | 'Passwords didn\' match!'), 1009 | (3, 0, 'Press any key')) 1010 | if self.any_key() == -1: 1011 | self.db = None 1012 | self.close() 1013 | if auth != 3: 1014 | self.db.keyfile = None 1015 | if password is False or confirm is False: 1016 | continue 1017 | if auth == 2 or auth == 3: 1018 | while True: 1019 | filepath = FileBrowser(self, False, True, None)() 1020 | if filepath is False: 1021 | break 1022 | elif filepath == -1: 1023 | self.close() 1024 | elif not isfile(filepath): 1025 | self.draw_text(False, 1026 | (1, 0, 'That\' not a file!'), 1027 | (3, 0, 'Press any key')) 1028 | if self.any_key() == -1: 1029 | self.db = None 1030 | self.close() 1031 | continue 1032 | break 1033 | if filepath is False: 1034 | continue 1035 | self.db.keyfile = filepath 1036 | if auth != 3: 1037 | self.db.password = None 1038 | 1039 | if auth is not False: 1040 | db = DBBrowser(self) 1041 | del db 1042 | last = self.cur_dir.split('/')[-1] 1043 | self.cur_dir = self.cur_dir[:-len(last) - 1] 1044 | else: 1045 | self.db = None 1046 | break 1047 | elif menu == 3: 1048 | self.remote_interface() 1049 | elif menu == 4: 1050 | self.gen_config_menu() 1051 | elif menu == 5 or menu is False or menu == -1: 1052 | self.close() 1053 | 1054 | def open_db(self, skip_fb=False): 1055 | ''' This method opens a database.''' 1056 | 1057 | if skip_fb is False: 1058 | filepath = FileBrowser(self, True, False, self.last_file)() 1059 | if filepath is False: 1060 | return False 1061 | elif filepath == -1: 1062 | self.close() 1063 | else: 1064 | self.cur_dir = filepath 1065 | 1066 | ret = self.get_authentication() 1067 | if ret is False: 1068 | return False 1069 | password, keyfile = ret 1070 | 1071 | try: 1072 | if isfile(self.cur_dir + '.lock'): 1073 | self.draw_text(False, 1074 | (1, 0, 'Database seems to be opened.' 1075 | ' Open file in read-only mode?' 1076 | ' [(y)/n]')) 1077 | while True: 1078 | try: 1079 | e = self.stdscr.getch() 1080 | except KeyboardInterrupt: 1081 | e = 4 1082 | 1083 | if e == ord('n'): 1084 | read_only = False 1085 | break 1086 | elif e == 4: 1087 | self.close() 1088 | elif e == cur.KEY_RESIZE: 1089 | self.resize_all() 1090 | elif e == cur.KEY_F5: 1091 | return False 1092 | else: 1093 | read_only = True 1094 | break 1095 | else: 1096 | read_only = False 1097 | self.db = KPDBv1(self.cur_dir, password, keyfile, read_only) 1098 | self.db.load() 1099 | return True 1100 | except KPError as err: 1101 | self.draw_text(False, 1102 | (1, 0, err.__str__()), 1103 | (4, 0, 'Press any key.')) 1104 | if self.any_key() == -1: 1105 | self.close() 1106 | last = self.cur_dir.split('/')[-1] 1107 | self.cur_dir = self.cur_dir[:-len(last) - 1] 1108 | return False 1109 | 1110 | def remote_interface(self, ask_for_agent = True, agent = False): 1111 | if ask_for_agent is True and agent is False: 1112 | use_agent = self.gen_menu(1, ((1, 0, 'Use agent (1)'), 1113 | (2, 0, 'Use no agent (2)'))) 1114 | elif agent is True: 1115 | use_agent = 1 1116 | else: 1117 | use_agent = 2 1118 | 1119 | if use_agent == 1: 1120 | port = self.get_num("Agent port: ", "50001", 5) 1121 | if port is False: 1122 | return False 1123 | elif port == -1: 1124 | self.close() 1125 | 1126 | sock = socket(AF_INET, SOCK_STREAM) 1127 | sock.settimeout(60) 1128 | try: 1129 | sock.connect(('localhost', port)) 1130 | sendmsg(sock, build_message((b'GET',))) 1131 | except OSError as err: 1132 | self.draw_text(False, (1, 0, err.__str__()), 1133 | (3, 0, "Press any key.")) 1134 | if self.any_key() == -1: 1135 | self.close() 1136 | return False 1137 | 1138 | db_buf = receive(sock) 1139 | if db_buf[:4] == b'FAIL' or db_buf[:4] == b'[Err': 1140 | self.draw_text(False, 1141 | (1, 0, db_buf), 1142 | (3, 0, 'Press any key.')) 1143 | if self.any_key() == -1: 1144 | self.close() 1145 | return False 1146 | sock.shutdown(SHUT_RDWR) 1147 | sock.close() 1148 | 1149 | sock = socket(AF_INET, SOCK_STREAM) 1150 | sock.settimeout(60) 1151 | try: 1152 | sock.connect(('localhost', port)) 1153 | sendmsg(sock, build_message((b'GETC',))) 1154 | except OSError as err: 1155 | self.draw_text(False, (1, 0, err.__str__()), 1156 | (3, 0, "Press any key.")) 1157 | if self.any_key() == -1: 1158 | self.close() 1159 | return False 1160 | 1161 | answer = receive(sock) 1162 | parts = answer.split(b'\xB2\xEA\xC0') 1163 | password = parts.pop(0).decode() 1164 | keyfile_cont = parts.pop(0).decode() 1165 | if keyfile_cont == '': 1166 | keyfile = None 1167 | else: 1168 | if not isdir('/tmp/keepassc'): 1169 | makedirs('/tmp/keepassc') 1170 | with open('/tmp/keepassc/tmp_keyfile', 'w') as handler: 1171 | handler.write(parts.pop(0).decode()) 1172 | keyfile = '/tmp/keepassc/tmp_keyfile' 1173 | 1174 | server = parts.pop(0).decode() 1175 | port = int(parts.pop(0)) 1176 | if parts.pop(0) == b'True': 1177 | ssl = True 1178 | else: 1179 | ssl = False 1180 | tls_dir = parts.pop(0).decode() 1181 | elif use_agent is False: 1182 | return False 1183 | elif use_agent == -1: 1184 | self.close() 1185 | else: 1186 | if isfile(self.remote_home): 1187 | with open(self.remote_home, 'r') as handler: 1188 | last_address = handler.readline() 1189 | last_port = handler.readline() 1190 | else: 1191 | last_address = '127.0.0.1' 1192 | last_port = None 1193 | 1194 | pass_auth = False 1195 | pass_ssl = False 1196 | while True: 1197 | if pass_auth is False: 1198 | ret = self.get_authentication() 1199 | if ret is False: 1200 | return False 1201 | elif ret == -1: 1202 | self.close() 1203 | password, keyfile = ret 1204 | pass_auth = True 1205 | if pass_ssl is False: 1206 | ssl = self.gen_menu(1, ((1, 0, 'Use SSL/TLS (1)'), 1207 | (2, 0, 'Plain text (2)'))) 1208 | if ssl is False: 1209 | pass_auth = False 1210 | continue 1211 | elif ssl == -1: 1212 | self.close() 1213 | pass_ssl = True 1214 | server = Editor(self.stdscr, max_text_size=1, 1215 | inittext=last_address, 1216 | win_location=(0, 1), 1217 | win_size=(1, self.xsize), 1218 | title="Server address")() 1219 | if server is False: 1220 | pass_ssl = False 1221 | continue 1222 | elif server == -1: 1223 | self.close() 1224 | if last_port is None: 1225 | if ssl == 1: 1226 | ssl = True # for later use 1227 | std_port = "50003" 1228 | else: 1229 | ssl = False 1230 | std_port = "50000" 1231 | else: 1232 | if ssl == 1: 1233 | ssl = True # for later use 1234 | else: 1235 | ssl = False 1236 | std_port = last_port 1237 | 1238 | port = self.get_num("Server port: ", std_port, 5) 1239 | if port is False: 1240 | path_auth = True 1241 | path_ssl = True 1242 | continue 1243 | elif port == -1: 1244 | self.close() 1245 | break 1246 | 1247 | if ssl is True: 1248 | try: 1249 | datapath = realpath(expanduser(getenv('XDG_DATA_HOME'))) 1250 | except: 1251 | datapath = realpath(expanduser('~/.local/share')) 1252 | finally: 1253 | tls_dir = join(datapath, 'keepassc') 1254 | else: 1255 | tls_dir = None 1256 | 1257 | client = Client(logging.INFO, 'client.log', server, port, 1258 | password, keyfile, ssl, tls_dir) 1259 | db_buf = client.get_db() 1260 | if db_buf[:4] == 'FAIL' or db_buf[:4] == "[Err": 1261 | self.draw_text(False, 1262 | (1, 0, db_buf), 1263 | (3, 0, 'Press any key.')) 1264 | if self.any_key() == -1: 1265 | self.close() 1266 | return False 1267 | self.db = KPDBv1(None, password, keyfile) 1268 | self.db.load(db_buf) 1269 | db = DBBrowser(self, True, server, port, ssl, tls_dir) 1270 | del db 1271 | return True 1272 | 1273 | def browser_help(self, mode_new): 1274 | '''Print help for filebrowser''' 1275 | 1276 | if mode_new: 1277 | self.draw_help( 1278 | 'Navigate with arrow keys.', 1279 | '\'o\' - choose directory', 1280 | '\'e\' - abort', 1281 | '\'H\' - show/hide hidden files', 1282 | '\'ngg\' - move to line n', 1283 | '\'G\' - move to last line', 1284 | '/text - go to \'text\' (like in vim/ranger)', 1285 | '\n', 1286 | 'Press return.') 1287 | else: 1288 | self.draw_help( 1289 | 'Navigate with arrow keys.', 1290 | '\'q\' - close program', 1291 | '\'e\' - abort', 1292 | '\'H\' - show/hide hidden files', 1293 | '\'ngg\' - move to line n', 1294 | '\'G\' - move to last line', 1295 | '/text - go to \'text\' (like in vim/ranger)', 1296 | '\n', 1297 | 'Press return.') 1298 | 1299 | def dbbrowser_help(self): 1300 | self.draw_help( 1301 | '\'e\' - go to main menu', 1302 | '\'q\' - close program', 1303 | '\'CTRL+D\' or \'CTRL+C\' - close program at any time', 1304 | '\'x\' - save db and close program', 1305 | '\'s\' - save db', 1306 | '\'S\' - save db with alternative filepath', 1307 | '\'c\' - copy password of current entry', 1308 | '\'b\' - copy username of current entry', 1309 | '\'H\' - show password of current entry', 1310 | '\'o\' - open URL of entry in standard webbrowser', 1311 | '\'P\' - edit db password', 1312 | '\'g\' - create group', 1313 | '\'G\' - create subgroup', 1314 | '\'y\' - create entry', 1315 | '\'d\' - delete group or entry (depends on what is marked)', 1316 | '\'t\' - edit title of selected group or entry', 1317 | '\'u\' - edit username', 1318 | '\'p\' - edit password', 1319 | '\'U\' - edit URL', 1320 | '\'C\' - edit comment', 1321 | '\'E\' - edit expiration date', 1322 | '\'f\' or \'/\' - find entry by title', 1323 | '\'L\' - lock db', 1324 | '\'m\' - enter move mode for marked group or entry', 1325 | '\'r\' - reload remote database (no function if not remote)', 1326 | 'Navigate with arrow keys or h/j/k/l like in vim', 1327 | 'Type \'return\' to enter subgroups', 1328 | 'Type \'backspace\' to go back to parent', 1329 | 'Type \'F5\' in a dialog to return to the previous one', 1330 | '\n', 1331 | 'Press return.') 1332 | 1333 | def move_help(self): 1334 | self.draw_help( 1335 | '\'e\' - go to main menu', 1336 | '\'q\' - close program', 1337 | '\'CTRL+D\' or \'CTRL+C\' - close program at any time', 1338 | 'Navigate up or down with arrow keys or k and j', 1339 | 'Navigate to subgroup with right arrow key or h', 1340 | 'Navigate to parent with left arrow key or l', 1341 | 'Type \'return\' to move the group to marked parent or the entry', 1342 | '\tto the marked group', 1343 | 'Type \'backspace\' to move a group to the root', 1344 | 'Type \'ESC\' to abort moving', 1345 | '\n', 1346 | 'Press return.') 1347 | 1348 | def show_dir(self, highlight, dir_cont): 1349 | '''List a directory with highlighting.''' 1350 | 1351 | self.draw_text(changed=False) 1352 | for i in range(len(dir_cont)): 1353 | if i == highlight: 1354 | if isdir(self.cur_dir + '/' + dir_cont[i]): 1355 | try: 1356 | self.stdscr.addstr( 1357 | i + 1, 0, dir_cont[i], cur.color_pair(5)) 1358 | except: 1359 | pass 1360 | else: 1361 | try: 1362 | self.stdscr.addstr( 1363 | i + 1, 0, dir_cont[i], cur.color_pair(3)) 1364 | except: 1365 | pass 1366 | else: 1367 | if isdir(self.cur_dir + '/' + dir_cont[i]): 1368 | try: 1369 | self.stdscr.addstr( 1370 | i + 1, 0, dir_cont[i], cur.color_pair(4)) 1371 | except: 1372 | pass 1373 | else: 1374 | try: 1375 | self.stdscr.addstr(i + 1, 0, dir_cont[i]) 1376 | except: 1377 | pass 1378 | self.stdscr.refresh() 1379 | 1380 | def close(self): 1381 | '''Close the program correctly.''' 1382 | 1383 | if self.config['rem_key'] is False and isfile(self.key_home): 1384 | remove(self.key_home) 1385 | cur.nocbreak() 1386 | self.stdscr.keypad(0) 1387 | cur.endwin() 1388 | exit() 1389 | 1390 | def show_groups(self, highlight, groups, cur_win, offset, changed, parent): 1391 | '''Just print all groups in a column''' 1392 | 1393 | self.draw_text(changed) 1394 | self.group_win.clear() 1395 | 1396 | if parent is self.db.root_group: 1397 | root_title = 'Parent: _ROOT_' 1398 | else: 1399 | root_title = 'Parent: ' + parent.title 1400 | if cur_win == 0: 1401 | h_color = 5 1402 | n_color = 4 1403 | else: 1404 | h_color = 6 1405 | n_color = 1 1406 | 1407 | try: 1408 | ysize = self.group_win.getmaxyx()[0] 1409 | self.group_win.addstr(0, 0, root_title, 1410 | cur.color_pair(n_color)) 1411 | if groups: 1412 | if len(groups) <= ysize - 3: 1413 | num = len(groups) 1414 | else: 1415 | num = ysize - 3 1416 | 1417 | for i in range(num): 1418 | if highlight == i + offset: 1419 | if groups[i + offset].children: 1420 | title = '+' + groups[i + offset].title 1421 | else: 1422 | title = ' ' + groups[i + offset].title 1423 | self.group_win.addstr(i + 1, 0, title, 1424 | cur.color_pair(h_color)) 1425 | else: 1426 | if groups[i + offset].children: 1427 | title = '+' + groups[i + offset].title 1428 | else: 1429 | title = ' ' + groups[i + offset].title 1430 | self.group_win.addstr(i + 1, 0, title, 1431 | cur.color_pair(n_color)) 1432 | x_of_n = str(highlight + 1) + ' of ' + str(len(groups)) 1433 | self.group_win.addstr(ysize - 2, 0, x_of_n) 1434 | except: 1435 | pass 1436 | finally: 1437 | self.group_win.refresh() 1438 | 1439 | def show_entries(self, e_highlight, entries, cur_win, offset): 1440 | '''Just print all entries in a column''' 1441 | 1442 | self.info_win.clear() 1443 | try: 1444 | self.entry_win.clear() 1445 | if entries: 1446 | if cur_win == 1: 1447 | h_color = 5 1448 | n_color = 4 1449 | else: 1450 | h_color = 6 1451 | n_color = 1 1452 | 1453 | ysize = self.entry_win.getmaxyx()[0] 1454 | if len(entries) <= ysize - 3: 1455 | num = len(entries) 1456 | else: 1457 | num = ysize - 3 1458 | 1459 | for i in range(num): 1460 | title = entries[i + offset].title 1461 | if date.today() > entries[i + offset].expire.date(): 1462 | expired = True 1463 | else: 1464 | expired = False 1465 | if e_highlight == i + offset: 1466 | if expired is True: 1467 | self.entry_win.addstr(i, 2, title, 1468 | cur.color_pair(3)) 1469 | else: 1470 | self.entry_win.addstr(i, 2, title, 1471 | cur.color_pair(h_color)) 1472 | else: 1473 | if expired is True: 1474 | self.entry_win.addstr(i, 2, title, 1475 | cur.color_pair(7)) 1476 | else: 1477 | self.entry_win.addstr(i, 2, title, 1478 | cur.color_pair(n_color)) 1479 | self.entry_win.addstr(ysize - 2, 2, (str(e_highlight + 1) + 1480 | ' of ' + 1481 | str(len(entries)))) 1482 | except: 1483 | pass 1484 | finally: 1485 | self.entry_win.noutrefresh() 1486 | 1487 | try: 1488 | if entries: 1489 | xsize = self.entry_win.getmaxyx()[1] 1490 | entry = entries[e_highlight] 1491 | if entry.title is None: 1492 | title = "" 1493 | elif len(entry.title) > xsize: 1494 | title = entry.title[:xsize - 2] + '\\' 1495 | else: 1496 | title = entry.title 1497 | if entry.group.title is None: 1498 | group_title = "" 1499 | elif len(entry.group.title) > xsize: 1500 | group_title = entry.group.title[:xsize - 9] + '\\' 1501 | else: 1502 | group_title = entry.group.title 1503 | if entry.username is None: 1504 | username = "" 1505 | elif len(entry.username) > xsize: 1506 | username = entry.username[:xsize - 12] + '\\' 1507 | else: 1508 | username = entry.username 1509 | if entry.url is None: 1510 | url = "" 1511 | elif len(entry.url) > xsize: 1512 | url = entry.title[:xsize - 7] + '\\' 1513 | else: 1514 | url = entry.url 1515 | if entry.creation is None: 1516 | creation = "" 1517 | else: 1518 | creation = entry.creation.__str__()[:10] 1519 | if entry.last_access is None: 1520 | last_access = "" 1521 | else: 1522 | last_access = entry.last_access.__str__()[:10] 1523 | if entry.last_mod is None: 1524 | last_mod = "" 1525 | else: 1526 | last_mod = entry.last_mod.__str__()[:10] 1527 | if entry.expire is None: 1528 | expire = "" 1529 | else: 1530 | if entry.expire.__str__()[:19] == '2999-12-28 23:59:59': 1531 | expire = "Expires: Never" 1532 | else: 1533 | expire = "Expires: " + entry.expire.__str__()[:10] 1534 | if entry.comment is None: 1535 | comment = "" 1536 | else: 1537 | comment = entry.comment 1538 | 1539 | self.info_win.addstr(2, 0, title, cur.A_BOLD) 1540 | self.info_win.addstr(3, 0, "Group: " + group_title) 1541 | self.info_win.addstr(4, 0, "Username: " + username) 1542 | self.info_win.addstr(5, 0, "URL: " + url) 1543 | self.info_win.addstr(6, 0, "Creation: " + creation) 1544 | self.info_win.addstr(7, 0, "Access: " + last_access) 1545 | self.info_win.addstr(8, 0, "Modification: " + last_mod) 1546 | self.info_win.addstr(9, 0, expire) 1547 | if date.today() > entry.expire.date(): 1548 | self.info_win.addstr(9, 22, ' (expired)') 1549 | if '\n' in comment: 1550 | comment = comment.split('\n')[0] 1551 | dots = ' ...' 1552 | else: 1553 | dots = '' 1554 | self.info_win.addstr(10, 0, "Comment: " + comment + dots) 1555 | except: 1556 | pass 1557 | finally: 1558 | self.info_win.noutrefresh() 1559 | cur.doupdate() 1560 | -------------------------------------------------------------------------------- /keepassc/daemon.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import time 4 | import atexit 5 | import signal 6 | 7 | class Daemon(object): 8 | """A generic daemon class. 9 | 10 | Usage: subclass the daemon class and override the run() method.""" 11 | 12 | def __init__(self, pidfile): self.pidfile = pidfile 13 | 14 | def daemonize(self): 15 | """Deamonize class. UNIX double fork mechanism.""" 16 | 17 | try: 18 | pid = os.fork() 19 | if pid > 0: 20 | # exit first parent 21 | sys.exit(0) 22 | except OSError as err: 23 | sys.stderr.write('fork #1 failed: {0}\n'.format(err)) 24 | sys.exit(1) 25 | 26 | # decouple from parent environment 27 | os.chdir('/') 28 | os.setsid() 29 | os.umask(0) 30 | 31 | # do second fork 32 | try: 33 | pid = os.fork() 34 | if pid > 0: 35 | 36 | # exit from second parent 37 | sys.exit(0) 38 | except OSError as err: 39 | sys.stderr.write('fork #2 failed: {0}\n'.format(err)) 40 | sys.exit(1) 41 | 42 | # redirect standard file descriptors 43 | sys.stdout.flush() 44 | sys.stderr.flush() 45 | si = open(os.devnull, 'r') 46 | so = open(os.devnull, 'a+') 47 | se = open(os.devnull, 'a+') 48 | 49 | os.dup2(si.fileno(), sys.stdin.fileno()) 50 | os.dup2(so.fileno(), sys.stdout.fileno()) 51 | os.dup2(se.fileno(), sys.stderr.fileno()) 52 | # write pidfile 53 | atexit.register(self.delpid) 54 | 55 | pid = str(os.getpid()) 56 | with open(self.pidfile,'w+') as f: 57 | f.write(pid + '\n') 58 | 59 | def delpid(self): 60 | os.remove(self.pidfile) 61 | 62 | def start(self): 63 | """Start the daemon.""" 64 | 65 | # Check for a pidfile to see if the daemon already runs 66 | try: 67 | with open(self.pidfile,'r') as pf: 68 | 69 | pid = int(pf.read().strip()) 70 | except IOError: 71 | pid = None 72 | 73 | if pid: 74 | message = "pidfile {0} already exist. " + \ 75 | "Daemon already running?\n" 76 | sys.stderr.write(message.format(self.pidfile)) 77 | sys.exit(1) 78 | 79 | # Start the daemon 80 | self.daemonize() 81 | self.run() 82 | 83 | def stop(self): 84 | """Stop the daemon.""" 85 | 86 | # Get the pid from the pidfile 87 | try: 88 | with open(self.pidfile,'r') as pf: 89 | pid = int(pf.read().strip()) 90 | except IOError: 91 | pid = None 92 | 93 | if not pid: 94 | message = "pidfile {0} does not exist. " + \ 95 | "Daemon not running?\n" 96 | sys.stderr.write(message.format(self.pidfile)) 97 | return # not an error in a restart 98 | 99 | # Try killing the daemon process 100 | try: 101 | while 1: 102 | os.kill(pid, signal.SIGTERM) 103 | time.sleep(0.1) 104 | except OSError as err: 105 | e = str(err.args) 106 | if e.find("No such process") > 0: 107 | if os.path.exists(self.pidfile): 108 | os.remove(self.pidfile) 109 | else: 110 | print (str(err.args)) 111 | sys.exit(1) 112 | 113 | def restart(self): 114 | """Restart the daemon.""" 115 | self.stop() 116 | self.start() 117 | 118 | def run(self): 119 | """You should override this method when you subclass Daemon. 120 | 121 | It will be called after the process has been daemonized by 122 | start() or restart().""" 123 | 124 | -------------------------------------------------------------------------------- /keepassc/editor.py: -------------------------------------------------------------------------------- 1 | """Scott Hansen 2 | 3 | Copyright (c) 2013, Scott Hansen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | """ 24 | 25 | import curses 26 | import curses.ascii 27 | import locale 28 | from textwrap import wrap 29 | from os import listdir 30 | from os.path import expanduser, isdir 31 | 32 | 33 | class Editor(object): 34 | """ Basic python curses text editor class. 35 | 36 | Can be used for multi-line editing. 37 | 38 | Text will be wrapped to the width of the editing window, so there will be 39 | no scrolling in the horizontal direction. For now, there's no line 40 | wrapping, so lines will have to be wrapped manually. 41 | 42 | Args: 43 | stdscr: the curses window object 44 | title: title text 45 | inittext: inital text content string 46 | win_location: tuple (y,x) for location of upper left corner 47 | win_size: tuple (rows,cols) size of the editor window 48 | box: True/False whether to outline editor with a box 49 | max_text_size: maximum rows allowed size for text. 50 | Default=0 (unlimited) 51 | pw_mode: True/False. Whether or not to show text entry 52 | (e.g. for passwords) 53 | 54 | Returns: 55 | text: text string or -1 on a KeyboardInterrupt 56 | 57 | Usage: 58 | import keepassc 59 | keepassc.editor(box=False, inittext="Hi", win_location=(5, 5)) 60 | 61 | TODO: fix pageup/pagedown for single line text entry 62 | 63 | """ 64 | 65 | def __init__(self, scr, title="", inittext="", win_location=(0, 0), 66 | win_size=(20, 80), box=True, max_text_size=0, pw_mode=False, 67 | quick_help=" (F2 or Enter: Save, F5: Cancel)", 68 | filebrowser = False): 69 | self.scr = scr 70 | self.title = title 71 | self.box = box 72 | self.quick_help = quick_help 73 | self.max_text_size = max_text_size 74 | self.filebrowser = filebrowser 75 | self.pw_mode = pw_mode 76 | if self.pw_mode is True: 77 | try: 78 | curses.curs_set(0) 79 | except: 80 | print('Invisible cursor not supported.') 81 | else: 82 | try: 83 | curses.curs_set(1) 84 | except: 85 | pass 86 | curses.echo() 87 | #locale.setlocale(locale.LC_ALL, '') 88 | curses.use_default_colors() 89 | #encoding = locale.getpreferredencoding() 90 | self.resize_flag = False 91 | self.win_location_x, self.win_location_y = win_location 92 | self.win_size_orig_y, self.win_size_orig_x = win_size 93 | self.win_size_y = self.win_size_orig_y 94 | self.win_size_x = self.win_size_orig_x 95 | self.win_init() 96 | self.box_init() 97 | self.text_init(inittext) 98 | self.keys_init() 99 | if filebrowser is True: 100 | self.filebrowser_init() 101 | self.display() 102 | 103 | def __call__(self): 104 | return self.run() 105 | 106 | def box_init(self): 107 | """Clear the main screen and redraw the box and/or title 108 | 109 | """ 110 | # Touchwin seems to save the underlying screen and refreshes it (for 111 | # example when the help popup is drawn and cleared again) 112 | self.scr.touchwin() 113 | self.scr.refresh() 114 | self.stdscr.clear() 115 | self.stdscr.refresh() 116 | if self.box is True: 117 | self.boxscr.clear() 118 | self.boxscr.box() 119 | if self.title: 120 | self.boxscr.addstr(1, 1, self.title, curses.A_BOLD) 121 | self.boxscr.addstr(self.quick_help, curses.A_STANDOUT) 122 | self.boxscr.addstr 123 | self.boxscr.refresh() 124 | elif self.title: 125 | self.boxscr.clear() 126 | self.boxscr.addstr(0, 0, self.title, curses.A_BOLD) 127 | self.boxscr.addstr(self.quick_help, curses.A_STANDOUT) 128 | self.boxscr.refresh() 129 | 130 | def text_init(self, text): 131 | """Transform text string into a list of strings, wrapped to fit the 132 | window size. Sets the dimensions of the text buffer. 133 | 134 | """ 135 | t = str(text).split('\n') 136 | if self.max_text_size != 1: 137 | t = [wrap(i, self.win_size_x - 1) for i in t] 138 | else: 139 | t = [t] 140 | self.text = [] 141 | for line in t: 142 | # This retains any empty lines 143 | if line: 144 | self.text.extend(line) 145 | else: 146 | self.text.append("") 147 | if self.text: 148 | # Sets size for text buffer...may be larger than win_size! 149 | self.buffer_cols = max(self.win_size_x, 150 | max([len(i) for i in self.text])) 151 | self.buffer_rows = max(self.win_size_y, len(self.text)) 152 | self.text_orig = self.text[:] 153 | if self.max_text_size > 1: 154 | # Truncates initial text if max_text_size < len(self.text) 155 | self.text = self.text[:self.max_text_size] 156 | self.buf_length = len(self.text[self.buffer_idx_y]) 157 | 158 | def keys_init(self): 159 | """Define methods for each key. 160 | 161 | """ 162 | self.keys = { 163 | curses.KEY_BACKSPACE: self.backspace, 164 | curses.KEY_DOWN: self.down, 165 | curses.KEY_END: self.end, 166 | curses.KEY_ENTER: self.insert_line_or_quit, 167 | curses.KEY_HOME: self.home, 168 | curses.KEY_DC: self.del_char, 169 | curses.KEY_LEFT: self.left, 170 | curses.KEY_NPAGE: self.page_down, 171 | curses.KEY_PPAGE: self.page_up, 172 | curses.KEY_RIGHT: self.right, 173 | curses.KEY_UP: self.up, 174 | curses.KEY_F1: self.help, 175 | curses.KEY_F2: self.quit, 176 | curses.KEY_F5: self.quit_nosave, 177 | curses.KEY_RESIZE: self.resize, 178 | chr(curses.ascii.ctrl(ord('x'))): self.quit, 179 | chr(curses.ascii.ctrl(ord('u'))): self.del_to_bol, 180 | chr(curses.ascii.ctrl(ord('k'))): self.del_to_eol, 181 | chr(curses.ascii.ctrl(ord('d'))): self.close, 182 | chr(curses.ascii.DEL): self.backspace, 183 | chr(curses.ascii.NL): self.insert_line_or_quit, 184 | chr(curses.ascii.LF): self.insert_line_or_quit, 185 | chr(curses.ascii.BS): self.backspace, 186 | chr(curses.ascii.ESC): self.quit_nosave, 187 | chr(curses.ascii.ETX): self.close, 188 | "\n": self.insert_line_or_quit, 189 | -1: self.resize, 190 | "\t": self.tab_completion 191 | } 192 | 193 | def win_init(self): 194 | """Set initial editor window size parameters, and reset them if window 195 | is resized. 196 | 197 | """ 198 | # self.cur_pos is the current y,x position of the cursor 199 | self.cur_pos_y = 0 200 | self.cur_pos_x = 0 201 | # y_offset controls the up-down scrolling feature 202 | self.x_offset = 0 203 | self.y_offset = 0 204 | self.buffer_idx_y = 0 205 | self.buffer_idx_x = 0 206 | # Adjust win_size if resizing 207 | if self.resize_flag is True: 208 | self.win_size_x += 1 209 | self.win_size_y += 1 210 | self.resize_flag = False 211 | # Make sure requested window size is < available window size 212 | self.max_win_size_y, self.max_win_size_x = self.scr.getmaxyx() 213 | # Adjust max_win_size for maximum possible offsets 214 | # (e.g. if there is a title and a box) 215 | self.max_win_size_y = max(0, self.max_win_size_y - 4) 216 | self.max_win_size_x = max(0, self.max_win_size_x - 3) 217 | # Keep the input box inside the physical window 218 | if (self.win_size_y > self.max_win_size_y or 219 | self.win_size_y < self.win_size_orig_y): 220 | self.win_size_y = self.max_win_size_y 221 | if (self.win_size_x > self.max_win_size_x or 222 | self.win_size_x < self.win_size_orig_x): 223 | self.win_size_x = self.max_win_size_x 224 | # Reduce win_size by 1 to account for position starting at 0 instead of 225 | # 1. E.g. if size=80, then the max size should be 79 (0-79). 226 | self.win_size_y -= 1 227 | self.win_size_x -= 1 228 | # Validate win_location settings 229 | if self.win_size_x + self.win_location_x >= self.max_win_size_x: 230 | self.win_location_x = max(0, self.max_win_size_x - 231 | self.win_size_x) 232 | if self.win_size_y + self.win_location_y >= self.max_win_size_y: 233 | self.win_location_y = max(0, self.max_win_size_y - 234 | self.win_size_y) 235 | # Create an extra window for the box outline and/or title, if required 236 | x_off = y_off = loc_off_y = loc_off_x = 0 237 | if self.box: 238 | y_off += 3 239 | x_off += 2 240 | loc_off_y += 1 241 | loc_off_x += 1 242 | if self.title: 243 | y_off += 1 244 | loc_off_y += 1 245 | if self.box is True or self.title: 246 | # Make box/title screen bigger than actual text area (stdscr) 247 | self.boxscr = self.scr.subwin(self.win_size_y + y_off, 248 | self.win_size_x + x_off, 249 | self.win_location_y, 250 | self.win_location_x) 251 | self.stdscr = self.boxscr.subwin(self.win_size_y, 252 | self.win_size_x, 253 | self.win_location_y + loc_off_y, 254 | self.win_location_x + loc_off_x) 255 | else: 256 | self.stdscr = self.scr.subwin(self.win_size_y, 257 | self.win_size_x, 258 | self.win_location_y, 259 | self.win_location_x) 260 | self.stdscr.keypad(1) 261 | 262 | def filebrowser_init(self): 263 | self.cur_dir = '' 264 | self.show = 0 265 | self.rem = [] 266 | 267 | def left(self): 268 | if (self.max_text_size == 1 and 269 | (self.cur_pos_x - self.x_offset) == 0 and self.x_offset > 0): 270 | self.cur_pos_x -= 1 271 | self.x_offset -= 1 272 | elif self.cur_pos_x > 0: 273 | self.cur_pos_x -= 1 274 | elif self.cur_pos_y > 0: 275 | self.up() 276 | self.buffer_idx_y = self.cur_pos_y + self.y_offset 277 | self.buf_length = len(self.text[self.buffer_idx_y]) 278 | self.end() 279 | 280 | def right(self): 281 | if self.cur_pos_x < self.win_size_x - 1: 282 | self.cur_pos_x += 1 283 | elif self.max_text_size == 1 and self.cur_pos_x < self.buf_length: 284 | self.cur_pos_x += 1 285 | if self.cur_pos_x == self.x_offset + self.win_size_x: 286 | self.x_offset += 1 287 | elif self.max_text_size != 1: 288 | self.cur_pos_x = 0 289 | self.down() 290 | 291 | def up(self): 292 | if self.cur_pos_y > 0: 293 | self.cur_pos_y = self.cur_pos_y - 1 294 | else: 295 | self.y_offset = max(0, self.y_offset - 1) 296 | 297 | def down(self): 298 | if (self.cur_pos_y < self.win_size_y - 1 and 299 | self.buffer_idx_y < len(self.text) - 1): 300 | self.cur_pos_y = self.cur_pos_y + 1 301 | elif self.buffer_idx_y == len(self.text) - 1: 302 | pass 303 | else: 304 | self.y_offset = min(self.buffer_rows - self.win_size_y, 305 | self.y_offset + 1) 306 | 307 | def end(self): 308 | self.cur_pos_x = self.buf_length 309 | if self.max_text_size == 1: 310 | self.x_offset = max(0, self.cur_pos_x - self.win_size_x + 1) 311 | 312 | def home(self): 313 | self.cur_pos_x = 0 314 | if self.max_text_size == 1: 315 | self.x_offset = 0 316 | 317 | def page_up(self): 318 | self.y_offset = max(0, self.y_offset - self.win_size_y) 319 | 320 | def page_down(self): 321 | self.y_offset = min(self.buffer_rows - self.win_size_y - 1, 322 | self.y_offset + self.win_size_y) 323 | # Corrects negative offsets 324 | self.y_offset = max(0, self.y_offset) 325 | 326 | def insert_char(self, c): 327 | """Given a curses wide character, insert that character in the current 328 | line. Stop when the maximum line length is reached. 329 | 330 | """ 331 | # Skip non-handled special characters (get_wch returns int value for 332 | # certain special characters) 333 | if isinstance(c, int): 334 | return 335 | line = list(self.text[self.buffer_idx_y]) 336 | line.insert(self.buffer_idx_x, c) 337 | if len(line) < self.win_size_x: 338 | if self.filebrowser is True: 339 | self.show = 0 340 | self.rem = [] 341 | self.cur_dir = "" 342 | if c == "~": 343 | line = line[:-1] 344 | line.insert(self.buffer_idx_x, expanduser("~/")) 345 | self.text[self.buffer_idx_y] = "".join(line) 346 | self.cur_pos_x += len(expanduser("~/")) 347 | self.x_offset = max(0, (len(self.text[self.buffer_idx_y]) 348 | - self.win_size_x) + 1) 349 | else: 350 | self.text[self.buffer_idx_y] = "".join(line) 351 | self.cur_pos_x += 1 352 | else: 353 | self.text[self.buffer_idx_y] = "".join(line) 354 | self.cur_pos_x += 1 355 | elif self.max_text_size == 1: 356 | if self.filebrowser is True: 357 | self.show = 0 358 | self.rem = [] 359 | self.cur_dir = "" 360 | if c == "~": 361 | line = line[:-1] 362 | line.insert(self.buffer_idx_x, expanduser("~/")) 363 | self.text[self.buffer_idx_y] = "".join(line) 364 | self.cur_pos_x += len(expanduser("~/")) 365 | self.x_offset += len(expanduser("~/")) 366 | else: 367 | self.text[self.buffer_idx_y] = "".join(line) 368 | self.cur_pos_x += 1 369 | self.x_offset += 1 370 | else: 371 | self.text[self.buffer_idx_y] = "".join(line) 372 | self.cur_pos_x += 1 373 | self.x_offset += 1 374 | else: 375 | if self.cur_pos_y < len(self.text) - 1: 376 | nline = self.text[self.cur_pos_y + 1] 377 | if len(self.text) != self.max_text_size: 378 | if self.cur_pos_y < len(self.text) - 1: 379 | self.text[self.cur_pos_y] = "".join(line[:-1]) 380 | self.text[self.cur_pos_y + 1] = ("".join(line[-1:])) + nline 381 | self.text_init("".join(self.text)) 382 | else: 383 | self.text[self.cur_pos_y] = "".join(line[:-1]) 384 | self.text.insert(self.cur_pos_y + 1, "".join(line[-1:])) 385 | if self.cur_pos_x < self.win_size_x - 2: 386 | self.cur_pos_x += 1 387 | else: 388 | self.down() 389 | self.cur_pos_x = 1 390 | self.buffer_rows = max(self.win_size_y, len(self.text)) 391 | 392 | def insert_line_or_quit(self): 393 | """Insert a new line at the cursor. Wrap text from the cursor to the 394 | end of the line to the next line. If the line is a single line, saves 395 | and exits. 396 | 397 | """ 398 | if self.max_text_size == 1: 399 | # Save and quit for single-line entries 400 | return False 401 | if len(self.text) == self.max_text_size: 402 | return 403 | line = list(self.text[self.buffer_idx_y]) 404 | newline = line[self.cur_pos_x:] 405 | line = line[:self.cur_pos_x] 406 | self.text[self.buffer_idx_y] = "".join(line) 407 | self.text.insert(self.buffer_idx_y + 1, "".join(newline)) 408 | self.buffer_rows = max(self.win_size_y, len(self.text)) 409 | self.cur_pos_x = 0 410 | self.down() 411 | 412 | def backspace(self): 413 | """Delete character under cursor and move one space left. 414 | 415 | """ 416 | line = list(self.text[self.buffer_idx_y]) 417 | if self.cur_pos_x > 0: 418 | if self.cur_pos_x <= len(line): 419 | # Just backspace if beyond the end of the actual string 420 | del line[self.buffer_idx_x - 1] 421 | self.text[self.buffer_idx_y] = "".join(line) 422 | self.cur_pos_x -= 1 423 | elif self.cur_pos_x == 0: 424 | # If at BOL, move cursor to end of previous line 425 | # (unless already at top of file) 426 | # If current or previous line is empty, delete it 427 | if self.y_offset > 0 or self.cur_pos_y > 0: 428 | self.cur_pos_x = len(self.text[self.buffer_idx_y - 1]) 429 | if not self.text[self.buffer_idx_y]: 430 | if len(self.text) > 1: 431 | del self.text[self.buffer_idx_y] 432 | elif not self.text[self.buffer_idx_y - 1]: 433 | del self.text[self.buffer_idx_y - 1] 434 | self.up() 435 | self.buffer_rows = max(self.win_size_y, len(self.text)) 436 | # Makes sure leftover rows are visually cleared if deleting rows from 437 | # the bottom of the text. 438 | self.stdscr.clear() 439 | 440 | if self.filebrowser is True: 441 | self.show = 0 442 | self.rem = [] 443 | self.cur_dir = "" 444 | 445 | def del_char(self): 446 | """Delete character under the cursor. 447 | 448 | """ 449 | line = list(self.text[self.buffer_idx_y]) 450 | if line and self.cur_pos_x < len(line): 451 | del line[self.buffer_idx_x] 452 | self.text[self.buffer_idx_y] = "".join(line) 453 | 454 | def del_to_eol(self): 455 | """Delete from cursor to end of current line. (C-k) 456 | 457 | """ 458 | line = list(self.text[self.buffer_idx_y]) 459 | line = line[:self.cur_pos_x] 460 | self.text[self.buffer_idx_y] = "".join(line) 461 | 462 | def del_to_bol(self): 463 | """Delete from cursor to beginning of current line. (C-u) 464 | 465 | """ 466 | line = list(self.text[self.buffer_idx_y]) 467 | line = line[self.cur_pos_x:] 468 | self.text[self.buffer_idx_y] = "".join(line) 469 | self.cur_pos_x = 0 470 | 471 | def tab_completion(self): 472 | if self.filebrowser is False: 473 | self.insert_char("\t") 474 | return 475 | 476 | if self.cur_dir == '': 477 | last = self.text[0].split('/')[-1] 478 | self.cur_dir = self.text[0][:-len(last)] 479 | try: 480 | dir_cont = listdir(self.cur_dir) 481 | except OSError: 482 | pass 483 | else: 484 | if len(self.rem) == 0: 485 | for i in dir_cont: 486 | if i[:len(last)] == last: 487 | self.rem.append(i) 488 | if len(self.rem) > 0: 489 | self.text[0] = self.cur_dir + self.rem[self.show] 490 | else: 491 | self.text[0] = self.cur_dir + last 492 | if self.show + 1 >= len(self.rem): 493 | self.show = 0 494 | else: 495 | self.show += 1 496 | if isdir(self.text[0]): 497 | self.text[0] += '/' 498 | self.buffer_idx_y = self.cur_pos_y + self.y_offset 499 | self.buf_length = len(self.text[self.buffer_idx_y]) 500 | self.end() 501 | 502 | def quit(self): 503 | return False 504 | 505 | def quit_nosave(self): 506 | self.text = False 507 | return False 508 | 509 | def help(self): 510 | """Display help text popup window. 511 | 512 | """ 513 | help_txt = """ 514 | Save and exit : F2 or Ctrl-x 515 | (Enter if single-line entry) 516 | Exit without saving : F5 or ESC 517 | Cursor movement : Arrow keys 518 | Move to beginning of line : Home 519 | Move to end of line : End 520 | Page Up/Page Down : PgUp/PgDn 521 | Backspace/Delete one char left of cursor : Backspace 522 | Delete 1 char under cursor : Del 523 | Insert line at cursor : Enter 524 | Delete to end of line : Ctrl-k 525 | Delete to beginning of line : Ctrl-u 526 | Help : F1 527 | """ 528 | try: 529 | curses.curs_set(0) 530 | except: 531 | pass 532 | txt = help_txt.split('\n') 533 | lines = min(self.max_win_size_y, len(txt) + 2) 534 | cols = min(self.max_win_size_x, max([len(i) for i in txt]) + 2) 535 | # Only print help text if the window is big enough 536 | try: 537 | popup = curses.newwin(lines, cols, 0, 0) 538 | popup.addstr(1, 1, help_txt) 539 | popup.box() 540 | except: 541 | pass 542 | else: 543 | while not popup.getch(): 544 | pass 545 | finally: 546 | # Turn back on the cursor 547 | if self.pw_mode is False: 548 | curses.curs_set(1) 549 | # flushinp Needed to prevent spurious F1 characters being written to line 550 | curses.flushinp() 551 | self.box_init() 552 | 553 | def resize(self): 554 | self.resize_flag = True 555 | self.win_init() 556 | self.box_init() 557 | self.text_init("\n".join(self.text)) 558 | 559 | def run(self): 560 | """Main program loop. 561 | 562 | """ 563 | try: 564 | while True: 565 | self.stdscr.move(self.cur_pos_y, (self.cur_pos_x - self.x_offset)) 566 | loop = self.get_key() 567 | if loop is False or loop == -1: 568 | break 569 | self.buffer_idx_y = self.cur_pos_y + self.y_offset 570 | self.buf_length = len(self.text[self.buffer_idx_y]) 571 | if self.cur_pos_x > self.buf_length: 572 | self.cur_pos_x = self.buf_length 573 | self.buffer_idx_x = self.cur_pos_x 574 | self.display() 575 | except KeyboardInterrupt: 576 | self.close() 577 | return self.exit() 578 | 579 | def display(self): 580 | """Display the editor window and the current contents. 581 | 582 | """ 583 | if self.max_text_size == 1: 584 | s = self.text[0][self.x_offset:(self.x_offset + self.win_size_x)] 585 | self.stdscr.move(0, 0) 586 | self.stdscr.clrtoeol() 587 | if not self.pw_mode: 588 | self.stdscr.addstr(0, 0, s) 589 | else: 590 | s = self.text[self.y_offset:(self.y_offset + self.win_size_y) or 1] 591 | for y, line in enumerate(s): 592 | try: 593 | self.stdscr.move(y, 0) 594 | self.stdscr.clrtoeol() 595 | if not self.pw_mode: 596 | self.stdscr.addstr(y, 0, line) 597 | except: 598 | self.close() 599 | self.stdscr.refresh() 600 | if self.box: 601 | self.boxscr.refresh() 602 | self.scr.refresh() 603 | 604 | def exit(self): 605 | """Normal exit procedure. 606 | 607 | """ 608 | curses.flushinp() 609 | try: 610 | curses.curs_set(0) 611 | except: # If invisible cursor not supported 612 | pass 613 | curses.noecho() 614 | if self.text == -1: 615 | return -1 616 | elif self.text is False: 617 | return False 618 | else: 619 | return "\n".join(self.text) 620 | 621 | def close(self): 622 | """Exiting on keyboard interrupt or other curses display errors. 623 | 624 | """ 625 | curses.endwin() 626 | self.text = -1 627 | return self.exit() 628 | 629 | def get_key(self): 630 | try: 631 | c = self.stdscr.get_wch() 632 | except KeyboardInterrupt: 633 | self.close() 634 | try: 635 | loop = self.keys[c]() 636 | except KeyError: 637 | self.insert_char(c) 638 | loop = True 639 | return loop 640 | 641 | 642 | def main(stdscr, **kwargs): 643 | return Editor(stdscr, **kwargs)() 644 | 645 | 646 | def editor(**kwargs): 647 | return curses.wrapper(main, **kwargs) 648 | -------------------------------------------------------------------------------- /keepassc/filebrowser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import curses as cur 4 | from curses.ascii import NL, DEL 5 | from os import listdir 6 | from os.path import expanduser, isdir 7 | 8 | from keepassc.editor import Editor 9 | 10 | class FileBrowser(object): 11 | '''This class represents the file browser''' 12 | 13 | def __init__(self, control, ask_for_lf, keyfile, last_file, mode_new = False): 14 | 15 | self.control = control 16 | self.ask_for_lf = ask_for_lf 17 | self.keyfile = keyfile 18 | self.last_file = last_file 19 | self.mode_new = mode_new 20 | self.highlight = 0 21 | self.kdb_file = None 22 | if self.control.cur_dir[-4:] == '.kdb': 23 | self.kdb_file = self.control.cur_dir.split('/')[-1] 24 | self.control.cur_dir = self.control.cur_dir[:-len(self.kdb_file) - 1] 25 | self.kdb_file = self.control.cur_dir + '/' + self.kdb_file 26 | self.hidden = True 27 | self.dir_cont = [] 28 | self.return_flag = False 29 | self.lookup = { 30 | cur.KEY_DOWN: self.nav_down, 31 | ord('j'): self.nav_down, 32 | cur.KEY_UP: self.nav_up, 33 | ord('k'): self.nav_up, 34 | cur.KEY_LEFT: self.nav_left, 35 | ord('h'): self.nav_left, 36 | cur.KEY_RIGHT: self.nav_right, 37 | ord('l'): self.nav_right, 38 | NL: self.nav_right, 39 | cur.KEY_RESIZE: self.control.resize_all, 40 | cur.KEY_F1: self.browser_help, 41 | ord('H'): self.show_hidden, 42 | ord('o'): self.open_file, 43 | cur.KEY_F5: self.cancel, 44 | ord('e'): self.cancel, 45 | 4: self.close, 46 | ord('q'): self.close, 47 | ord('G'): self.G_typed, 48 | ord('/'): self.find} 49 | self.find_rem = [] 50 | self.find_pos = 0 51 | 52 | def __call__(self): 53 | ret = self.get_filepath() 54 | if self.kdb_file is not None: 55 | self.control.cur_dir = self.kdb_file 56 | return ret 57 | 58 | def get_filepath(self): 59 | '''This method is used to get a filepath, e.g. for 'Save as' ''' 60 | 61 | if (self.ask_for_lf is False or self.last_file is None or 62 | self.control.config['rem_db'] is False): 63 | nav = self.control.gen_menu(1, ( 64 | (1, 0, 'Use the file browser (1)'), 65 | (2, 0, 'Type direct path (2)'))) 66 | else: 67 | nav = self.control.gen_menu(1, ( 68 | (1, 0, 'Use ' + self.last_file + ' (1)'), 69 | (2, 0, 'Use the file browser (2)'), 70 | (3, 0, 'Type direct path (3)'))) 71 | if ((self.ask_for_lf is True and self.last_file is not None and 72 | nav == 2) or 73 | ((self.last_file is None or self.ask_for_lf is False) and 74 | nav == 1)): 75 | if self.keyfile is True: 76 | filepath = self.browser() 77 | else: 78 | filepath = self.browser() 79 | if type(filepath) is str: 80 | if filepath[-4:] != '.kdb' and filepath is not False: 81 | filename = Editor(self.control.stdscr, max_text_size=1, 82 | win_location=(0, 1), win_size=(1, 80), 83 | title="Filename: ")() 84 | if filename == "": 85 | return False 86 | filepath += '/' + filename + '.kdb' 87 | return filepath 88 | if ((self.ask_for_lf is True and self.last_file is not None and 89 | nav == 3) or 90 | ((self.last_file is None or self.ask_for_lf is False) and 91 | nav == 2)): 92 | filepath = '' 93 | while True: 94 | if self.last_file: 95 | init = self.last_file 96 | else: 97 | init = '' 98 | filepath = self.get_direct_filepath(filepath) 99 | if filepath is False: 100 | return False 101 | elif filepath == -1: 102 | return -1 103 | elif ((filepath[-4:] != '.kdb' or isdir(filepath)) and 104 | self.keyfile is False): 105 | self.control.draw_text(False, 106 | (1, 0, 'Need path to a kdb-file!'), 107 | (3, 0, 'Press any key')) 108 | if self.control.any_key() == -1: 109 | return -1 110 | continue 111 | else: 112 | return filepath 113 | elif nav == 1: # it was asked for last file 114 | return self.last_file 115 | elif nav == -1: 116 | return -1 117 | else: 118 | return False 119 | 120 | def get_direct_filepath(self, filepath): 121 | '''Get a direct filepath.''' 122 | 123 | return Editor(self.control.stdscr, max_text_size=1, 124 | win_location=(0, 1), 125 | win_size=(1, 80), title="Direct filepath: ", 126 | inittext = filepath, filebrowser = True)() 127 | 128 | def nav_down(self): 129 | '''Navigate down''' 130 | 131 | if self.highlight < len(self.dir_cont) - 1: 132 | self.highlight += 1 133 | 134 | def nav_up(self): 135 | '''Navigate up''' 136 | 137 | if self.highlight > 0: 138 | self.highlight -= 1 139 | 140 | def nav_left(self): 141 | '''Navigate left''' 142 | 143 | last = self.control.cur_dir.split('/')[-1] 144 | self.control.cur_dir = self.control.cur_dir[:-len(last) - 1] 145 | if self.control.cur_dir == '': 146 | self.control.cur_dir = '/' 147 | self.highlight = 0 148 | self.get_dir_cont() 149 | self.find_rem = [] 150 | self.find_pos = 0 151 | 152 | def nav_right(self): 153 | '''Navigate right''' 154 | 155 | self.find_rem = [] 156 | self.find_pos = 0 157 | if self.dir_cont[self.highlight] == '..': 158 | last = self.control.cur_dir.split('/')[-1] 159 | self.control.cur_dir = self.control.cur_dir[:-len(last) - 1] 160 | if self.control.cur_dir == '': 161 | self.control.cur_dir = '/' 162 | self.highlight = 0 163 | self.get_dir_cont() 164 | elif isdir(self.control.cur_dir + '/' + self.dir_cont[self.highlight]): 165 | self.control.cur_dir = (self.control.cur_dir + '/' + 166 | self.dir_cont[self.highlight]) 167 | if self.control.cur_dir[:2] == '//': 168 | self.control.cur_dir = self.control.cur_dir[1:] 169 | self.highlight = 0 170 | self.get_dir_cont() 171 | else: 172 | ret = self.control.cur_dir + '/' + self.dir_cont[self.highlight] 173 | if self.kdb_file is not None: 174 | self.control.cur_dir = self.kdb_file 175 | self.return_flag = True 176 | return ret 177 | 178 | def show_hidden(self): 179 | '''Show hidden files''' 180 | 181 | if self.hidden is True: 182 | self.hidden = False 183 | else: 184 | self.hidden = True 185 | self.get_dir_cont() 186 | 187 | def browser_help(self): 188 | '''Show help''' 189 | 190 | self.control.browser_help(self.mode_new) 191 | 192 | def open_file(self): 193 | '''Return dir or file for "save as..."''' 194 | 195 | if self.mode_new is True: 196 | if self.kdb_file is not None: 197 | ret = self.control.cur_dir 198 | self.control.cur_dir = self.kdb_file 199 | self.return_flag = True 200 | return ret 201 | else: 202 | self.return_flag = True 203 | return self.control.cur_dir 204 | 205 | def cancel(self): 206 | '''Cancel browser''' 207 | 208 | self.return_flag = True 209 | return False 210 | 211 | def close(self): 212 | '''Close KeePassC''' 213 | 214 | self.return_flag = True 215 | return -1 216 | 217 | def start_gg(self, c): 218 | '''Enable gg like in vim''' 219 | 220 | gg = chr(c) 221 | while True: 222 | try: 223 | c = self.control.stdscr.getch() 224 | except KeyboardInterrupt: 225 | c = 4 226 | 227 | if gg[-1] == 'g' and c == ord('g') and gg[:-1] != '': 228 | if int(gg[:-1]) > len(self.dir_cont): 229 | self.highlight = len(self.dir_cont) -1 230 | else: 231 | self.highlight = int(gg[:-1]) -1 232 | return True 233 | elif gg[-1] == 'g' and c == ord('g') and gg[:-1] == '': 234 | self.highlight = 0 235 | return True 236 | elif gg[-1] != 'g' and c == ord('g'): 237 | gg += 'g' 238 | elif 48 <= c <= 57 and gg[-1] != 'g': 239 | gg += chr(c) 240 | elif c in self.lookup: 241 | return c 242 | 243 | def G_typed(self): 244 | '''G typed => last entry (like in vim)''' 245 | 246 | self.highlight = len(self.dir_cont) - 1 247 | 248 | def find(self): 249 | '''Find a directory or file like in ranger''' 250 | 251 | filename = Editor(self.control.stdscr, max_text_size=1, 252 | win_location=(0, 1), win_size=(1, 80), 253 | title="Filename to find: ")() 254 | if filename == '' and self.find_pos < len(self.find_rem) - 1: 255 | self.find_pos += 1 256 | elif filename == '': 257 | self.find_pos = 0 258 | else: 259 | self.find_rem = [] 260 | self.find_pos = 0 261 | for i in self.dir_cont: 262 | if filename.lower() in i.lower(): 263 | self.find_rem.append(i) 264 | if self.find_rem: 265 | self.highlight = self.dir_cont.index(self.find_rem[self.find_pos]) 266 | 267 | def browser(self): 268 | '''A simple file browser.''' 269 | 270 | self.get_dir_cont() 271 | if self.dir_cont == -1 or self.dir_cont is False: 272 | return self.dir_cont 273 | 274 | old_highlight = None 275 | while True: 276 | if old_highlight != self.highlight or self.highlight == 0: 277 | self.control.show_dir(self.highlight, self.dir_cont) 278 | try: 279 | c = self.control.stdscr.getch() 280 | except KeyboardInterrupt: 281 | c = 4 282 | 283 | if 49 <= c <= 57 or c == ord('g'): 284 | c = self.start_gg(c) 285 | 286 | old_highlight = self.highlight 287 | if c in self.lookup: 288 | ret = self.lookup[c]() 289 | if self.return_flag is True: 290 | return ret 291 | 292 | def get_dir_cont(self): 293 | '''Get the content of the current dir''' 294 | 295 | try: 296 | dir_cont = listdir(self.control.cur_dir) 297 | except OSError: 298 | self.control.draw_text(False, 299 | (1, 0, 'Was not able to read directory'), 300 | (2, 0, 'Press any key.')) 301 | if self.control.any_key() == -1: 302 | return -1 303 | last = self.control.cur_dir.split('/')[-1] 304 | self.control.cur_dir = self.control.cur_dir[:-len(last) - 1] 305 | if self.control.cur_dir == '': 306 | self.control.cur_dir = '/' 307 | return False 308 | 309 | rem = [] 310 | for i in dir_cont: 311 | if ((not isdir(self.control.cur_dir + '/' + i) and not 312 | i[-4:] == '.kdb' and self.keyfile is False) or 313 | (i[0] == '.' and self.hidden is True)): 314 | rem.append(i) 315 | for i in rem: 316 | dir_cont.remove(i) 317 | 318 | dirs = [] 319 | files = [] 320 | for i in dir_cont: 321 | if isdir(self.control.cur_dir + '/' + i): 322 | dirs.append(i) 323 | else: 324 | files.append(i) 325 | dirs.sort() 326 | files.sort() 327 | 328 | self.dir_cont = [] 329 | self.dir_cont.extend(dirs) 330 | self.dir_cont.extend(files) 331 | if not self.control.cur_dir == '/': 332 | self.dir_cont.insert(0, '..') 333 | -------------------------------------------------------------------------------- /keepassc/helper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import struct 4 | from os import makedirs, remove 5 | from os.path import isdir, isfile 6 | 7 | from Cryptodome.Hash import SHA256 8 | from Cryptodome.Cipher import AES 9 | 10 | def parse_config(control): 11 | '''Parse the config file. 12 | 13 | It's important that a line in the file is written without spaces, 14 | that means 15 | 16 | - 'foo=bar' is a valid line 17 | - 'foo = bar' is not a valid one 18 | 19 | ''' 20 | config = {'del_clip': True, # standard config 21 | 'clip_delay': 20, 22 | 'lock_db': True, 23 | 'lock_delay': 60, 24 | 'rem_db': True, 25 | 'rem_key': False, 26 | 'skip_menu': False, 27 | 'pin': True} 28 | 29 | if isfile(control.config_home): 30 | try: 31 | handler = open(control.config_home, 'r') 32 | except Exception as err: # don't know if this is good style 33 | print(err.__str__()) 34 | else: 35 | for line in handler: 36 | key, val = line.split('=') 37 | if val == 'True\n': 38 | val = True 39 | elif val == 'False\n': 40 | val = False 41 | else: 42 | val = int(val) 43 | if key in config: 44 | config[key] = val 45 | handler.close() 46 | else: # write standard config 47 | write_config(control, config) 48 | return config 49 | 50 | 51 | def write_config(control, config): 52 | '''Function to write the config file''' 53 | 54 | config_dir = control.config_home[:-7] 55 | if not isdir(config_dir): 56 | if isfile(config_dir): 57 | remove(config_dir) 58 | makedirs(config_dir) 59 | try: 60 | handler = open(control.config_home, 'w') 61 | except Exception as err: 62 | print(err.__str__()) 63 | return False 64 | else: 65 | for key, val in config.items(): 66 | handler.write(key + '=' + str(val) + '\n') 67 | handler.close() 68 | return True 69 | 70 | def transform_key(masterkey, seed1, seed2, rounds): 71 | """This method creates the key to decrypt the database""" 72 | 73 | if masterkey is None or seed1 is None or seed2 is None or rounds is None: 74 | raise TypeError('None type not allowed') 75 | aes = AES.new(seed1, AES.MODE_ECB) 76 | 77 | # Encrypt the created hash 78 | for i in range(rounds): 79 | masterkey = aes.encrypt(masterkey) 80 | 81 | # Finally, hash it again... 82 | sha_obj = SHA256.new() 83 | sha_obj.update(masterkey) 84 | masterkey = sha_obj.digest() 85 | # ...and hash the result together with the randomseed 86 | sha_obj = SHA256.new() 87 | sha_obj.update(seed2 + masterkey) 88 | return sha_obj.digest() 89 | 90 | def get_passwordkey(key): 91 | """This method hashes key""" 92 | 93 | if key is None: 94 | raise TypeError('None type not allowed') 95 | sha = SHA256.new() 96 | sha.update(key.encode('utf-8')) 97 | return sha.digest() 98 | 99 | def get_filekey(keyfile): 100 | """This method creates a key from a keyfile.""" 101 | 102 | try: 103 | handler = open(keyfile, 'rb') 104 | buf = handler.read() 105 | except: 106 | raise OSError('Could not open or read file.') 107 | else: 108 | handler.close() 109 | sha = SHA256.new() 110 | if len(buf) == 33: 111 | sha.update(buf) 112 | return sha.digest() 113 | elif len(buf) == 65: 114 | sha.update(struct.unpack('<65s', buf)[0].decode()) 115 | return sha.digest() 116 | else: 117 | while buf: 118 | if len(buf) <= 2049: 119 | sha.update(buf) 120 | buf = [] 121 | else: 122 | sha.update(buf[:2048]) 123 | buf = buf[2048:] 124 | return sha.digest() 125 | 126 | def get_remote_filekey(buf): 127 | """This method creates a key from a keyfile.""" 128 | 129 | sha = SHA256.new() 130 | if len(buf) == 33: 131 | sha.update(buf) 132 | return sha.digest() 133 | elif len(buf) == 65: 134 | sha.update(struct.unpack('<65s', buf)[0].decode()) 135 | return sha.digest() 136 | else: 137 | while buf: 138 | if len(buf) <= 2049: 139 | sha.update(buf) 140 | buf = [] 141 | else: 142 | sha.update(buf[:2048]) 143 | buf = buf[2048:] 144 | return sha.digest() 145 | 146 | def get_key(password, keyfile, remote = False): 147 | """Get a key generated from KeePass-password and -keyfile""" 148 | 149 | if password is None and keyfile is None: 150 | raise TypeError('None type not allowed') 151 | elif password is None: 152 | if remote is True: 153 | masterkey = get_remote_filekey(keyfile) 154 | else: 155 | masterkey = get_filekey(keyfile) 156 | elif password is not None and keyfile is not None: 157 | passwordkey = get_passwordkey(password) 158 | if remote is True: 159 | filekey = get_remote_filekey(keyfile) 160 | else: 161 | filekey = get_filekey(keyfile) 162 | sha = SHA256.new() 163 | sha.update(passwordkey+filekey) 164 | masterkey = sha.digest() 165 | else: 166 | masterkey = get_passwordkey(password) 167 | 168 | return masterkey 169 | 170 | -------------------------------------------------------------------------------- /keepassc/server.py: -------------------------------------------------------------------------------- 1 | """This file implements the server daemon. 2 | 3 | Decorator: 4 | class waitDecorator(object) 5 | 6 | Classes: 7 | class Server(Connection, Daemon) 8 | """ 9 | 10 | import logging 11 | import signal 12 | import socket 13 | import ssl 14 | import sys 15 | import time 16 | import threading 17 | from datetime import datetime 18 | from os import chdir 19 | from os.path import join, expanduser, realpath 20 | 21 | from kppy.database import KPDBv1 22 | from kppy.exceptions import KPError 23 | 24 | from keepassc.conn import * 25 | from keepassc.daemon import Daemon 26 | from keepassc.helper import get_key, transform_key 27 | 28 | class waitDecorator(object): 29 | def __init__(self, func): 30 | self.func = func 31 | self.lock = False 32 | 33 | def __get__(self, obj, type=None): 34 | return self.__class__(self.func.__get__(obj, type)) 35 | 36 | def __call__(self, *args): 37 | while True: 38 | if self.lock == True: 39 | time.sleep(1) 40 | continue 41 | else: 42 | self.lock = True 43 | self.func(args[0], args[1]) 44 | self.lock = False 45 | break 46 | 47 | class Server(Daemon): 48 | """The KeePassC server daemon""" 49 | 50 | def __init__(self, pidfile, loglevel, logfile, address = None, 51 | port = 50002, db = None, password = None, keyfile = None, 52 | tls = False, tls_dir = None, tls_port = 50003, 53 | tls_req = False): 54 | Daemon.__init__(self, pidfile) 55 | 56 | try: 57 | logdir = realpath(expanduser(getenv('XDG_DATA_HOME'))) 58 | except: 59 | logdir = realpath(expanduser('~/.local/share')) 60 | finally: 61 | logfile = join(logdir, 'keepassc', logfile) 62 | 63 | logging.basicConfig(format='[%(levelname)s] in %(filename)s:' 64 | '%(funcName)s at %(asctime)s\n%(message)s', 65 | level=loglevel, filename=logfile, 66 | filemode='a') 67 | 68 | if db is None: 69 | print('Need a database path') 70 | sys.exit(1) 71 | 72 | self.db_path = realpath(expanduser(db)) 73 | 74 | # To use this idiom only once, I store the keyfile path 75 | # as a class attribute 76 | if keyfile is not None: 77 | keyfile = realpath(expanduser(keyfile)) 78 | else: 79 | keyfile = None 80 | 81 | chdir("/var/empty") 82 | 83 | try: 84 | self.db = KPDBv1(self.db_path, password, keyfile) 85 | self.db.load() 86 | except KPError as err: 87 | print(err) 88 | logging.error(err.__str__()) 89 | sys.exit(1) 90 | 91 | self.lookup = { 92 | b'FIND': self.find, 93 | b'GET': self.send_db, 94 | b'CHANGESECRET': self.change_password, 95 | b'NEWG': self.create_group, 96 | b'NEWE': self.create_entry, 97 | b'DELG': self.delete_group, 98 | b'DELE': self.delete_entry, 99 | b'MOVG': self.move_group, 100 | b'MOVE': self.move_entry, 101 | b'TITG': self.set_g_title, 102 | b'TITE': self.set_e_title, 103 | b'USER': self.set_e_user, 104 | b'URL': self.set_e_url, 105 | b'COMM': self.set_e_comment, 106 | b'PASS': self.set_e_pass, 107 | b'DATE': self.set_e_exp} 108 | 109 | self.sock = None 110 | self.net_sock = None 111 | self.tls_sock = None 112 | self.tls_req = tls_req 113 | 114 | if tls is True or tls_req is True: 115 | self.context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) 116 | cert = join(tls_dir, "servercert.pem") 117 | key = join(tls_dir, "serverkey.pem") 118 | self.context.load_cert_chain(certfile=cert, keyfile=key) 119 | else: 120 | self.context = None 121 | 122 | try: 123 | # Listen for commands 124 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 125 | self.sock.bind(("localhost", 50000)) 126 | self.sock.listen(5) 127 | except OSError as err: 128 | print(err) 129 | logging.error(err.__str__()) 130 | sys.exit(1) 131 | else: 132 | logging.info('Server socket created on localhost:50000') 133 | 134 | if self.tls_req is False and address is not None: 135 | try: 136 | # Listen for commands 137 | self.net_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 138 | self.net_sock.bind((address, port)) 139 | self.net_sock.listen(5) 140 | except OSError as err: 141 | print(err) 142 | logging.error(err.__str__()) 143 | sys.exit(1) 144 | else: 145 | logging.info('Server socket created on '+address+':'+ 146 | str(port)) 147 | 148 | if self.context is not None and address is not None: 149 | try: 150 | # Listen for commands 151 | self.tls_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 152 | self.tls_sock.bind((address, tls_port)) 153 | self.tls_sock.listen(5) 154 | except OSError as err: 155 | print(err) 156 | logging.error(err.__str__()) 157 | sys.exit(1) 158 | else: 159 | logging.info('TLS-Server socket created on '+address+':'+ 160 | str(tls_port)) 161 | 162 | 163 | #Handle SIGTERM 164 | signal.signal(signal.SIGTERM, self.handle_sigterm) 165 | 166 | def check_password(self, password, keyfile): 167 | """Check received password""" 168 | 169 | master = get_key(password, keyfile, True) 170 | remote_final = transform_key(master, self.db._transf_randomseed, 171 | self.db._final_randomseed, 172 | self.db._key_transf_rounds) 173 | master = get_key(self.db.password, self.db.keyfile) 174 | final = transform_key(master, self.db._transf_randomseed, 175 | self.db._final_randomseed, 176 | self.db._key_transf_rounds) 177 | return (remote_final == final) 178 | 179 | def run(self): 180 | """Overide Daemon.run() and provide socets""" 181 | 182 | try: 183 | local_thread = threading.Thread(target=self.handle_non_tls, 184 | args=(self.sock,)) 185 | local_thread.start() 186 | if self.tls_req is False: 187 | non_tls_thread = threading.Thread(target=self.handle_non_tls, 188 | args=(self.net_sock,)) 189 | non_tls_thread.start() 190 | if self.context is not None: 191 | tls_thread = threading.Thread(target=self.handle_tls) 192 | tls_thread.start() 193 | except OSError as err: 194 | logging.error(err.__str__()) 195 | self.stop() 196 | 197 | def handle_non_tls(self, sock): 198 | while True: 199 | try: 200 | conn, client = sock.accept() 201 | except OSError as err: 202 | # For correct closing 203 | if "Bad file descriptor" in err.__str__(): 204 | break 205 | logging.error(err.__str__()) 206 | else: 207 | logging.info('Connection from '+client[0]+':'+str(client[1])) 208 | client_thread = threading.Thread(target=self.handle_client, 209 | args=(conn,client,)) 210 | client_thread.daemon = True 211 | client_thread.start() 212 | 213 | def handle_tls(self): 214 | while True: 215 | try: 216 | conn_tmp, client = self.tls_sock.accept() 217 | conn = self.context.wrap_socket(conn_tmp, server_side = True) 218 | except (ssl.SSLError, OSError) as err: 219 | # For correct closing 220 | if "Bad file descriptor" in err.__str__(): 221 | break 222 | logging.error(err.__str__()) 223 | else: 224 | logging.info('Connection from '+client[0]+':'+str(client[1])) 225 | client_thread = threading.Thread(target=self.handle_client, 226 | args=(conn, client,)) 227 | client_thread.daemon = True 228 | client_thread.start() 229 | 230 | def handle_client(self, conn, client): 231 | conn.settimeout(60) 232 | 233 | try: 234 | msg = receive(conn) 235 | parts = msg.split(b'\xB2\xEA\xC0') 236 | parts.append(client) 237 | password = parts.pop(0) 238 | keyfile = parts.pop(0) 239 | cmd = parts.pop(0) 240 | 241 | if password == b'': 242 | password = None 243 | else: 244 | password = password.decode() 245 | if keyfile == b'': 246 | keyfile = None 247 | if self.check_password(password, keyfile) is False: 248 | sendmsg(conn, b'FAIL: Wrong password') 249 | raise OSError("Received wrong password") 250 | except OSError as err: 251 | logging.error(err.__str__()) 252 | else: 253 | try: 254 | if cmd in self.lookup: 255 | self.lookup[cmd](conn, parts) 256 | else: 257 | logging.error('Received a wrong command') 258 | sendmsg(conn, b'FAIL: Command isn\'t available') 259 | except (OSError, ValueError) as err: 260 | logging.error(err.__str__()) 261 | finally: 262 | conn.shutdown(socket.SHUT_RDWR) 263 | conn.close() 264 | 265 | def find(self, conn, parts): 266 | """Find entries and send them to connection""" 267 | 268 | title = parts.pop(0) 269 | msg = '' 270 | for i in self.db.entries: 271 | if title.decode().lower() in i.title.lower(): 272 | msg += 'Title: '+i.title+'\n' 273 | if i.url is not None: 274 | msg += 'URL: '+i.url+'\n' 275 | if i.username is not None: 276 | msg += 'Username: '+i.username+'\n' 277 | if i.password is not None: 278 | msg += 'Password: '+i.password+'\n' 279 | if i.creation is not None: 280 | msg += 'Creation: '+i.creation.__str__()+'\n' 281 | if i.last_access is not None: 282 | msg += 'Access: '+i.last_access.__str__()+'\n' 283 | if i.last_mod is not None: 284 | msg += 'Modification: '+i.last_mod.__str__()+'\n' 285 | if i.expire is not None: 286 | msg += 'Expiration: '+i.expire.__str__()+'\n' 287 | if i.comment is not None: 288 | msg += 'Comment: '+i.comment+'\n' 289 | msg += '\n' 290 | sendmsg(conn, msg.encode()) 291 | 292 | def send_db(self, conn, parts): 293 | with open(self.db_path, 'rb') as handler: 294 | buf = handler.read() 295 | sendmsg(conn, buf) 296 | 297 | @waitDecorator 298 | def create_group(self, conn, parts): 299 | title = parts.pop(0).decode() 300 | root = int(parts.pop(0)) 301 | if root == 0: 302 | self.db.create_group(title) 303 | else: 304 | for i in self.db.groups: 305 | if i.id_ == root: 306 | self.db.create_group(title, i) 307 | break 308 | elif i is self.db.groups[-1]: 309 | sendmsg(conn, b"FAIL: Parent doesn't exist anymore. " 310 | b"You should refresh") 311 | return 312 | self.db.save() 313 | self.send_db(conn, []) 314 | 315 | @waitDecorator 316 | def change_password(self, conn, parts): 317 | client_add = parts[-1][0] 318 | if client_add != "localhost" and client_add != "127.0.0.1": 319 | sendmsg(conn, b'Password change from remote is not allowed') 320 | 321 | new_password = parts.pop(0).decode() 322 | new_keyfile = parts.pop(0).decode() 323 | if new_password == '': 324 | self.db.password = None 325 | else: 326 | self.db.password = new_password 327 | 328 | if new_keyfile == '': 329 | self.db.keyfile = None 330 | else: 331 | self.db.keyfile = realpath(expanduser(new_keyfile)) 332 | 333 | self.db.save() 334 | sendmsg(conn, b"Password changed") 335 | 336 | @waitDecorator 337 | def create_entry(self, conn, parts): 338 | title = parts.pop(0).decode() 339 | url = parts.pop(0).decode() 340 | username = parts.pop(0).decode() 341 | password = parts.pop(0).decode() 342 | comment = parts.pop(0).decode() 343 | y = int(parts.pop(0)) 344 | mon = int(parts.pop(0)) 345 | d = int(parts.pop(0)) 346 | root = int(parts.pop(0)) 347 | 348 | for i in self.db.groups: 349 | if i.id_ == root: 350 | self.db.create_entry(i, title, 1, url, username, password, 351 | comment, y, mon, d) 352 | break 353 | elif i is self.db.groups[-1]: 354 | sendmsg(conn, b"FAIL: Group for entry doesn't exist " 355 | b"anymore. You should refresh") 356 | return 357 | 358 | self.db.save() 359 | self.send_db(conn, []) 360 | 361 | @waitDecorator 362 | def delete_group(self, conn, parts): 363 | group_id = int(parts.pop(0)) 364 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 365 | int(parts[3]), int(parts[4]), int(parts[5])) 366 | time = time.timetuple() 367 | 368 | for i in self.db.groups: 369 | if i.id_ == group_id: 370 | if self.check_last_mod(i, time) is True: 371 | sendmsg(conn, b"FAIL: Group was modified. You should " 372 | b"refresh and if you're sure you want " 373 | b"to delete this group try it again.") 374 | return 375 | i.remove_group() 376 | break 377 | elif i is self.db.groups[-1]: 378 | sendmsg(conn, b"FAIL: Group doesn't exist " 379 | b"anymore. You should refresh") 380 | return 381 | 382 | self.db.save() 383 | self.send_db(conn, []) 384 | 385 | @waitDecorator 386 | def delete_entry(self, conn, parts): 387 | uuid = parts.pop(0) 388 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 389 | int(parts[3]), int(parts[4]), int(parts[5])) 390 | time = time.timetuple() 391 | 392 | for i in self.db.entries: 393 | if i.uuid == uuid: 394 | if self.check_last_mod(i, time) is True: 395 | sendmsg(conn, b"FAIL: Entry was modified. You should " 396 | b"refresh and if you're sure you want " 397 | b"to delete this entry try it again.") 398 | return 399 | i.remove_entry() 400 | break 401 | elif i is self.db.entries[-1]: 402 | sendmsg(conn, b"FAIL: Entry doesn't exist " 403 | b"anymore. You should refresh") 404 | return 405 | 406 | self.db.save() 407 | self.send_db(conn, []) 408 | 409 | @waitDecorator 410 | def move_group(self, conn, parts): 411 | group_id = int(parts.pop(0)) 412 | root = int(parts.pop(0)) 413 | 414 | for i in self.db.groups: 415 | if i.id_ == group_id: 416 | if root == 0: 417 | i.move_group(self.db.root_group) 418 | else: 419 | for j in self.db.groups: 420 | if j.id_ == root: 421 | i.move_group(j) 422 | break 423 | elif j is self.db.groups[-1]: 424 | sendmsg(conn, b"FAIL: New parent doesn't " 425 | b"exist anymore. You should " 426 | b"refresh") 427 | return 428 | break 429 | elif i is self.db.groups[-1]: 430 | sendmsg(conn, b"FAIL: Group doesn't exist " 431 | b"anymore. You should refresh") 432 | return 433 | 434 | self.db.save() 435 | self.send_db(conn, []) 436 | 437 | @waitDecorator 438 | def move_entry(self, conn, parts): 439 | uuid = parts.pop(0) 440 | root = int(parts.pop(0)) 441 | 442 | for i in self.db.entries: 443 | if i.uuid == uuid: 444 | for j in self.db.groups: 445 | if j.id_ == root: 446 | i.move_entry(j) 447 | break 448 | elif j is self.db.groups[-1]: 449 | sendmsg(conn, b"FAIL: New parent doesn't exist " 450 | b"anymore. You should refresh") 451 | return 452 | break 453 | elif i is self.db.entries[-1]: 454 | sendmsg(conn, b"FAIL: Entry doesn't exist " 455 | b"anymore. You should refresh") 456 | return 457 | 458 | self.db.save() 459 | self.send_db(conn, []) 460 | 461 | @waitDecorator 462 | def set_g_title(self, conn, parts): 463 | title = parts.pop(0).decode() 464 | group_id = int(parts.pop(0)) 465 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 466 | int(parts[3]), int(parts[4]), int(parts[5])) 467 | time = time.timetuple() 468 | 469 | for i in self.db.groups: 470 | if i.id_ == group_id: 471 | if self.check_last_mod(i, time) is True: 472 | sendmsg(conn, b"FAIL: Group was modified. You should " 473 | b"refresh and if you're sure you want " 474 | b"to edit this group try it again.") 475 | return 476 | i.set_title(title) 477 | break 478 | elif i is self.db.groups[-1]: 479 | sendmsg(conn, b"FAIL: Group doesn't exist " 480 | b"anymore. You should refresh") 481 | return 482 | 483 | self.db.save() 484 | self.send_db(conn, []) 485 | 486 | @waitDecorator 487 | def set_e_title(self, conn, parts): 488 | title = parts.pop(0).decode() 489 | uuid = parts.pop(0) 490 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 491 | int(parts[3]), int(parts[4]), int(parts[5])) 492 | time = time.timetuple() 493 | 494 | for i in self.db.entries: 495 | if i.uuid == uuid: 496 | if self.check_last_mod(i, time) is True: 497 | sendmsg(conn, b"FAIL: Entry was modified. You should " 498 | b"refresh and if you're sure you want " 499 | b"to edit this entry try it again.") 500 | return 501 | i.set_title(title) 502 | break 503 | elif i is self.db.entries[-1]: 504 | sendmsg(conn, b"FAIL: Entry doesn't exist " 505 | b"anymore. You should refresh") 506 | return 507 | 508 | self.db.save() 509 | self.send_db(conn, []) 510 | 511 | @waitDecorator 512 | def set_e_user(self, conn, parts): 513 | username = parts.pop(0).decode() 514 | uuid = parts.pop(0) 515 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 516 | int(parts[3]), int(parts[4]), int(parts[5])) 517 | time = time.timetuple() 518 | 519 | for i in self.db.entries: 520 | if i.uuid == uuid: 521 | if self.check_last_mod(i, time) is True: 522 | sendmsg(conn, b"FAIL: Entry was modified. You should " 523 | b"refresh and if you're sure you want " 524 | b"to edit this entry try it again.") 525 | return 526 | i.set_username(username) 527 | break 528 | elif i is self.db.entries[-1]: 529 | sendmsg(conn, b"FAIL: Entry doesn't exist " 530 | b"anymore. You should refresh") 531 | return 532 | 533 | self.db.save() 534 | self.send_db(conn, []) 535 | 536 | @waitDecorator 537 | def set_e_url(self, conn, parts): 538 | url = parts.pop(0).decode() 539 | uuid = parts.pop(0) 540 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 541 | int(parts[3]), int(parts[4]), int(parts[5])) 542 | time = time.timetuple() 543 | 544 | for i in self.db.entries: 545 | if i.uuid == uuid: 546 | if self.check_last_mod(i, time) is True: 547 | sendmsg(conn, b"FAIL: Entry was modified. You should " 548 | b"refresh and if you're sure you want " 549 | b"to edit this entry try it again.") 550 | return 551 | i.set_url(url) 552 | break 553 | elif i is self.db.entries[-1]: 554 | sendmsg(conn, b"FAIL: Entry doesn't exist " 555 | b"anymore. You should refresh") 556 | return 557 | 558 | self.db.save() 559 | self.send_db(conn, []) 560 | 561 | @waitDecorator 562 | def set_e_comment(self, conn, parts): 563 | comment = parts.pop(0).decode() 564 | uuid = parts.pop(0) 565 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 566 | int(parts[3]), int(parts[4]), int(parts[5])) 567 | time = time.timetuple() 568 | 569 | for i in self.db.entries: 570 | if i.uuid == uuid: 571 | if self.check_last_mod(i, time) is True: 572 | sendmsg(conn, b"FAIL: Entry was modified. You should " 573 | b"refresh and if you're sure you want " 574 | b"to edit this entry try it again.") 575 | return 576 | i.set_comment(comment) 577 | break 578 | elif i is self.db.entries[-1]: 579 | sendmsg(conn, b"FAIL: Entry doesn't exist " 580 | b"anymore. You should refresh") 581 | return 582 | 583 | self.db.save() 584 | self.send_db(conn, []) 585 | 586 | @waitDecorator 587 | def set_e_pass(self, conn, parts): 588 | password = parts.pop(0).decode() 589 | uuid = parts.pop(0) 590 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 591 | int(parts[3]), int(parts[4]), int(parts[5])) 592 | time = time.timetuple() 593 | 594 | for i in self.db.entries: 595 | if i.uuid == uuid: 596 | if self.check_last_mod(i, time) is True: 597 | sendmsg(conn, b"FAIL: Entry was modified. You should " 598 | b"refresh and if you're sure you want " 599 | b"to edit this entry try it again.") 600 | return 601 | i.set_password(password) 602 | break 603 | elif i is self.db.entries[-1]: 604 | sendmsg(conn, b"FAIL: Entry doesn't exist " 605 | b"anymore. You should refresh") 606 | return 607 | 608 | self.db.save() 609 | self.send_db(conn, []) 610 | 611 | @waitDecorator 612 | def set_e_exp(self, conn, parts): 613 | y = int(parts.pop(0)) 614 | mon = int(parts.pop(0)) 615 | d = int(parts.pop(0)) 616 | uuid = parts.pop(0) 617 | time = datetime(int(parts[0]), int(parts[1]), int(parts[2]), 618 | int(parts[3]), int(parts[4]), int(parts[5])) 619 | time = time.timetuple() 620 | 621 | for i in self.db.entries: 622 | if i.uuid == uuid: 623 | if self.check_last_mod(i, time) is True: 624 | sendmsg(conn, b"FAIL: Entry was modified. You should " 625 | b"refresh and if you're sure you want " 626 | b"to edit this entry try it again.") 627 | return 628 | i.set_expire(y, mon, d) 629 | break 630 | elif i is self.db.entries[-1]: 631 | sendmsg(conn, b"FAIL: Entry doesn't exist " 632 | b"anymore. You should refresh") 633 | return 634 | 635 | self.db.save() 636 | self.send_db(conn, []) 637 | 638 | def check_last_mod(self, obj, time): 639 | return obj.last_mod.timetuple() > time 640 | 641 | def handle_sigterm(self, signum, frame): 642 | self.db.lock() 643 | if self.sock is not None: 644 | self.sock.shutdown(socket.SHUT_RDWR) 645 | self.sock.close() 646 | if self.net_sock is not None: 647 | self.net_sock.shutdown(socket.SHUT_RDWR) 648 | self.net_sock.close() 649 | if self.tls_sock is not None: 650 | self.tls_sock.shutdown(socket.SHUT_RDWR) 651 | self.tls_sock.close() 652 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import mkdir, stat 2 | from stat import ST_MODE 3 | 4 | from distutils.core import setup 5 | 6 | setup(name = "keepassc", 7 | version = "1.8.2", 8 | author = "Karsten-Kai König, Scott Hansen", 9 | author_email = "grayfox@outerhaven.de", 10 | url = "http://raymontag.github.com/keepassc", 11 | download_url = "https://github.com/raymontag/keepassc/tarball/master", 12 | description = "A password manager that is fully compatible to KeePass v.1.x and KeePassX", 13 | packages = ['keepassc'], 14 | scripts = ['bin/keepassc', 'bin/keepassc-server', 'bin/keepassc-agent'], 15 | install_requires = ['kppy', 'pycryptodomex'], 16 | classifiers = [ 17 | 'Programming Language :: Python :: 3.3', 18 | 'Operating System :: POSIX', 19 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 20 | 'Development Status :: 5 - Production/Stable', 21 | 'Environment :: Console :: Curses'], 22 | license = "ISC, MIT", 23 | data_files = [('share/man/man1', ['keepassc.1', 'keepassc-server.1', 'keepassc-agent.1']), 24 | ('share/doc/keepassc', ['README.md', 'LICENSE.md', 'CHANGELOG'])], 25 | ) 26 | --------------------------------------------------------------------------------