├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── bin └── pwm ├── pwm ├── __init__.py ├── pwm.py └── test_pwm.py ├── readme.md ├── setup.py └── travis └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | .idea/* 55 | pwm.egg-info 56 | pwm_tool.egg-info 57 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://travis-ci.org/lovedboy/pwm 2 | language: python 3 | python: 4 | - 2.7.8 5 | - 2.7 6 | - 3.3 7 | - 3.4 8 | - 3.5 9 | - nightly 10 | branches: 11 | only: 12 | - master 13 | script: 14 | - python -m unittest discover 15 | - python setup.py install 16 | - cd travis 17 | - export PWM_DB_PATH="pwm.db" 18 | - /bin/sh setup.sh 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.py 3 | include *.rst 4 | -------------------------------------------------------------------------------- /bin/pwm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | import pwm 5 | pwm.main() 6 | -------------------------------------------------------------------------------- /pwm/__init__.py: -------------------------------------------------------------------------------- 1 | from .pwm import * 2 | -------------------------------------------------------------------------------- /pwm/pwm.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | from hashlib import sha1 4 | from base64 import b64encode 5 | import sys 6 | import hmac 7 | import re 8 | import sqlite3 9 | import os 10 | import getpass 11 | from optparse import OptionParser 12 | import sys 13 | is_py2 = sys.version_info[0] == 2 14 | 15 | 16 | __author = 'ls0f' 17 | __version = '0.3' 18 | __package = 'pwm' 19 | 20 | 21 | class PWM(object): 22 | 23 | def __init__(self, key, db_path=None): 24 | self.key = key 25 | self.passwd_length = 15 26 | self.db_path = db_path 27 | self.table = 'pwm' 28 | 29 | def gen_passwd(self, raw): 30 | 31 | if sys.version_info > (3, 0): 32 | h = hmac.new(self.key.encode(), raw.encode(), sha1) 33 | base64 = b64encode(h.digest()).decode() 34 | else: 35 | h = hmac.new(self.key, raw, sha1) 36 | base64 = h.digest().encode("base64") 37 | _passwd = base64[0: self.passwd_length] 38 | return self._format_passwd(_passwd) 39 | 40 | def _format_passwd(self, passwd): 41 | # 格式化密码,必须包含大小写和数字 42 | self.num_str = "0123456789" 43 | self.low_letters = "abcdefghijklmnopqrstuvwxyz" 44 | self.upper_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 45 | passwd = passwd.replace("+", '0') 46 | passwd = passwd.replace("/", '1') 47 | list_passwd = list(passwd) 48 | 49 | if re.search(r"[0-9]", passwd) is None: 50 | list_passwd[-3] = self.num_str[ord(passwd[-3]) % len(self.num_str)] 51 | 52 | if re.search(r"[a-z]", passwd) is None: 53 | list_passwd[-2] = self.low_letters[ord(passwd[-2]) % len( 54 | self.low_letters)] 55 | 56 | if re.search(r"[A-Z]", passwd) is None: 57 | list_passwd[-1] = self.upper_letters[ord(passwd[-1]) % len( 58 | self.upper_letters)] 59 | 60 | return ''.join(list_passwd) 61 | 62 | def _get_conn(self): 63 | if self.db_path is None: 64 | print ("You didn't set you PWD_DB_PATH ENV") 65 | sys.exit(1) 66 | try: 67 | conn = sqlite3.connect(self.db_path) 68 | except sqlite3.OperationalError: 69 | print ("PWD_DB_PATH: {} is invalid! (Is it a directory or a file?)".format(self.db_path)) 70 | sys.exit(1) 71 | return conn 72 | 73 | def __enter__(self): 74 | self.conn = self._get_conn() 75 | 76 | def __exit__(self, exc_type, exc_val, exc_tb): 77 | 78 | if exc_type: 79 | self.conn.rollback() 80 | else: 81 | self.conn.commit() 82 | self.conn.close() 83 | 84 | def _create_table(self): 85 | sql = """ 86 | create table if not exists {}( 87 | `id` INTEGER PRIMARY KEY, 88 | `domain` varchar(32) , 89 | `account` varchar(32), 90 | `batch` varchar(4) default NULL 91 | ); 92 | """.format(self.table) 93 | 94 | with self: 95 | cur = self.conn.cursor() 96 | cur.execute(sql) 97 | sql = "PRAGMA table_info(pwm);" 98 | cur.execute(sql) 99 | for cid, name, _, _, _, _ in cur.fetchall(): 100 | if name == "batch": 101 | return 102 | else: 103 | sql = "ALTER TABLE %s ADD COLUMN batch varchar(4);" % ( 104 | self.table, ) 105 | cur.execute(sql) 106 | 107 | def _insert_account(self, domain, account, batch=None): 108 | sql = "insert into {} (domain, account, batch) values "\ 109 | "('{}', '{}', '{}');".format( 110 | self.table, domain, account, batch) 111 | with self: 112 | cur = self.conn.cursor() 113 | cur.execute(sql,) 114 | 115 | def _query_account(self, keyword): 116 | if keyword: 117 | query = " where domain like '%{}%' or account like '%{}%' ".format( 118 | keyword, keyword) 119 | else: 120 | query = "" 121 | 122 | sql = "select id,domain,account,batch from {} {}".format( 123 | self.table, query) 124 | # print sql 125 | 126 | with self: 127 | cur = self.conn.cursor() 128 | cur.execute(sql) 129 | result = cur.fetchall() 130 | return result 131 | 132 | def _delete(self, id): 133 | 134 | sql = "delete from {} where id={}".format(self.table, id) 135 | with self: 136 | cur = self.conn.cursor() 137 | cur.execute(sql) 138 | row_count = cur.rowcount 139 | return row_count 140 | 141 | def insert(self, domain, account, batch): 142 | self._create_table() 143 | self._insert_account(domain, account, batch or '') 144 | print("save success") 145 | 146 | @staticmethod 147 | def gen_sign_raw(domain, account, batch): 148 | if is_py2: 149 | raw = "{}@{}".format(account.encode("utf-8"), domain.encode("utf-8")) 150 | else: 151 | raw = "{}@{}".format(account, domain) 152 | if batch: 153 | raw = "{}@{}".format(raw, str(batch)) 154 | return raw 155 | 156 | def gen_account_passwd(self, domain, account, batch): 157 | 158 | raw = self.gen_sign_raw(domain, account, batch) 159 | return self.gen_passwd(raw) 160 | 161 | def delete(self, id): 162 | self._create_table() 163 | row_count = self._delete(id) 164 | print("remove success, {} record(s) removed".format(row_count, )) 165 | 166 | def search(self, keyword): 167 | 168 | self._create_table() 169 | if keyword == '*': 170 | keyword = '' 171 | 172 | result = self._query_account(keyword) 173 | return result 174 | 175 | 176 | def main(): 177 | 178 | db_path = os.getenv("PWM_DB_PATH", None) 179 | if db_path is None: 180 | print("##########WARNING:############") 181 | print("You didn't set you PWD_DB_PATH ENV") 182 | print("echo \"export PWM_DB_PATH=your_path\" >> ~/.bashrc") 183 | print("source ~/.bashrc") 184 | print("###############################") 185 | parse = OptionParser(version="{} {}".format(__package, __version)) 186 | 187 | parse.add_option('-k', '--key', help="your secret key", nargs=0) 188 | parse.add_option('-d', '--domain', help="the domain of you account") 189 | parse.add_option('-a', '--account', help="the account used to login") 190 | parse.add_option( 191 | '-s', '--search', 192 | help="list your account and domain by search keyword") 193 | parse.add_option( 194 | '-w', '--save', 195 | help="save your account and domain", nargs=0) 196 | parse.add_option( 197 | '-r', '--remove', 198 | help="remove your account and domain by id", nargs=1, type=int) 199 | parse.add_option( 200 | '-b', '--batch', 201 | help="add batch to generate different passwd with same domain and account", # noqa 202 | nargs=1, type=int) 203 | parse.add_option( 204 | '-c', '--copy', 205 | help="copy password to clipboard", nargs=0) 206 | (options, args) = parse.parse_args() 207 | 208 | if options.copy is not None: 209 | try: 210 | import xerox 211 | except ImportError: 212 | print ("you should install xerox module") 213 | sys.exit(1) 214 | 215 | if options.key is not None: 216 | key = getpass.getpass(prompt="your key:") 217 | else: 218 | key = '' 219 | 220 | pwm = PWM(key=key, db_path=db_path) 221 | 222 | # 搜索 223 | if options.search: 224 | result = pwm.search(options.search.strip()) 225 | fmt = "{:5s}|{:40s}|{:35s}|{:20s}|{:5s}" 226 | print(fmt.format("ID", "DOMAIN", "ACCOUNT", "PASWORD", "BATCH")) 227 | print(fmt.format("-"*5, "-"*40, "-"*35, "-"*20, "-"*5)) 228 | for item in result: 229 | passwd = pwm.gen_account_passwd(item[1], item[2], item[3]) 230 | if options.copy is not None: 231 | xerox.copy(passwd) 232 | if is_py2: 233 | print(fmt.format(str(item[0]), item[1].encode("utf-8"), item[2].encode("utf-8"), passwd, item[3])) 234 | else: 235 | print(fmt.format(str(item[0]), item[1], item[2], passwd, item[3] or '')) 236 | 237 | 238 | print("\n{} records found.\n".format(len(result))) 239 | return 240 | 241 | # 删除 242 | if options.remove: 243 | pwm.delete(options.remove) 244 | return 245 | 246 | # 生成密码 247 | if bool(options.domain) is False or bool(options.account) is False: 248 | parse.print_help() 249 | return 250 | 251 | passwd = pwm.gen_account_passwd( 252 | options.domain, options.account, options.batch) 253 | if options.copy is not None: 254 | xerox.copy(passwd) 255 | print("generate password:{}".format(passwd)) 256 | 257 | # 保存 258 | if options.save is not None: 259 | pwm.insert(options.domain, options.account, options.batch) 260 | 261 | 262 | if __name__ == "__main__": 263 | main() 264 | -------------------------------------------------------------------------------- /pwm/test_pwm.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | from unittest import TestCase 4 | from pwm import PWM 5 | import re 6 | import os 7 | 8 | 9 | class TestPwm(TestCase): 10 | 11 | def setUp(self): 12 | self.db_path = os.path.join(os.path.dirname(__file__), "tmp.dat") 13 | self.p = PWM(key='123456', db_path= self.db_path) 14 | 15 | def tearDown(self): 16 | if os.path.exists(self.db_path): 17 | os.unlink(os.path.join(os.path.dirname(__file__), "tmp.dat")) 18 | 19 | def test_gen_passwd(self): 20 | 21 | passwd = self.p.gen_passwd('1234') 22 | self.assertTrue(re.search(r"[0-9]", passwd) is not None) 23 | self.assertTrue(re.search(r"[a-z]", passwd) is not None) 24 | self.assertTrue(re.search(r"[A-Z]", passwd) is not None) 25 | 26 | def test_insert(self): 27 | self.p.insert(domain='github.com', account='lovedboy', batch='') 28 | res = self.p._query_account('github.com') 29 | self.assertEqual(res, [(1, 'github.com', 'lovedboy', '')]) 30 | 31 | def test_search(self): 32 | self.p.insert(domain='github.com', account='lovedboy', batch='') 33 | self.p.search('*') 34 | 35 | def test_delete(self): 36 | self.p.insert(domain='github.com', account='lovedboy', batch='') 37 | res = self.p._query_account('github.com') 38 | self.assertEqual(res, [(1, 'github.com', 'lovedboy', '')]) 39 | self.p.delete(1) 40 | res = self.p._query_account('github.com') 41 | self.assertEqual(res, []) 42 | 43 | def test_gen_account_password(self): 44 | 45 | p = self.p.gen_account_passwd('github.com', 'lovedboy', '') 46 | self.assertEqual(p, 'lcrv5vttjqOk7c3') 47 | 48 | 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 密码管理工具。 3 | 4 | ### 原理 5 | 6 | 思路来源于[花密](http://flowerpassword.com/)。 7 | 算法[hmac](http://baike.baidu.com/item/hmac)。 8 | 9 | 签名字符串用命令行参数中的**域名**和**账号**拼接生成。签名秘钥来源于用户输入,生成签名后转为base64编码,基于一定规则生成一个包含大小写字母和数字的15位密码。 10 | 11 | 12 | ### 使用 13 | 14 | ##### 安装 15 | 16 | `pip install pwm-tool` 17 | 18 | 19 | #### 生成密码 20 | 21 | 默认使用空字符串作为签名秘钥。 22 | 23 | `⇒ pwm -d github.com -a ls0f` 24 | 25 | 使用自己的秘钥签名(**-k**): 26 | 27 | ``` 28 | ⇒ pwm -d github.com -a ls0f -k 29 | your key: 30 | ``` 31 | 32 | #### 保存域名和账号 33 | 34 | 首先你需要配置数据库的保存路径。 35 | 36 | ``` 37 | echo "export PWM_DB_PATH=your_path" >> ~/.bashrc 38 | source ~/.bashrc 39 | ``` 40 | 41 | `pwm -d github.com -a ls0f -w` 42 | 43 | 这里只会保存域名和账号,方便搜索。密码都是通过密钥算出来的。 44 | 45 | #### 搜索 46 | 47 | 可基于账号和域名模糊搜索: 48 | 49 | ``` 50 | pwm -s ls0f 51 | pwm -s github.com -k 52 | ``` 53 | 54 | #### 安全性 55 | 56 | 保证你签名秘钥的安全!!! 57 | 58 | #### 在线生成 59 | 60 | [https://ls0f.github.io/pwm/](https://ls0f.github.io/pwm/) 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | from setuptools import setup, find_packages 4 | 5 | PACKAGE = "pwm-tool3" 6 | NAME = "pwm-tool3" 7 | DESCRIPTION = "password manager tool" 8 | AUTHOR = "lovedboy" 9 | AUTHOR_EMAIL = "lovedboy.tk@qq.com" 10 | URL = "https://github.com/ls0f/pwm" 11 | VERSION = '0.3.2' 12 | 13 | setup( 14 | name=NAME, 15 | version=VERSION, 16 | description=DESCRIPTION, 17 | author=AUTHOR, 18 | author_email=AUTHOR_EMAIL, 19 | license="BSD", 20 | url=URL, 21 | package_data={'': ['*.txt', '*.TXT']}, 22 | include_package_data=True, 23 | scripts=['bin/pwm'], 24 | packages=find_packages(), 25 | classifiers=[ 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python', 28 | 'Intended Audience :: Developers', 29 | 'Operating System :: OS Independent', 30 | ], 31 | zip_safe=False, 32 | ) 33 | -------------------------------------------------------------------------------- /travis/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; 4 | 5 | # check pwm exists; 6 | pwm; 7 | 8 | # save; 9 | 10 | pwm -d github.com -a lovedboy -w; 11 | 12 | # search; 13 | 14 | pwm -s github | grep github.com; 15 | pwm -s github | grep LNXStZoEGuHi9rb; 16 | pwm -s github | grep "1 records"; 17 | 18 | # delete; 19 | 20 | pwm -r 1 | grep '1 record(s) removed'; 21 | pwm -s github | grep "0 records"; 22 | --------------------------------------------------------------------------------