test-ly-files: Display symlink creation, add `-v` to `ln`
[sunny256-utils.git] / genpasswd
blob1794d3567201d3f09bfc1ea14a8b876733ee9de3
1 #!/usr/bin/python3
2 """Generate Password from Salts and Passphrase.
4 The salts come from command line option and the passphrase from stdin.
5 Print ``hash(salts + passphrase)'' for your password.
6 You can generate different passwords for many domains from one passphrase.
8 usage: genpasswd [options] [string...]
10 example:
11     $ genpasswd example.com         # domain to login
12     Passphrase:My Passphrase        # without echoback
13     Vn/aHPNgXbieJCkSGYiAA7y9GwM     # got your password for example.com
15     $ genpasswd example.net         # domain to login
16     Passphrase:My Passphrase        # without echoback
17     /+/G4MzuaiSo9dHE/c0+GgPi6Nc     # got your password for example.net
18 """
20 # Copyright (c) 2009-2021 Satoshi Fukutomi <info@fuktommy.com>.
21 # All rights reserved.
23 # Redistribution and use in source and binary forms, with or without
24 # modification, are permitted provided that the following conditions
25 # are met:
26 # 1. Redistributions of source code must retain the above copyright
27 #    notice, this list of conditions and the following disclaimer.
28 # 2. Redistributions in binary form must reproduce the above copyright
29 #    notice, this list of conditions and the following disclaimer in the
30 #    documentation and/or other materials provided with the distribution.
32 # THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
33 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
34 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
35 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
36 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
37 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
38 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
39 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
40 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
41 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
42 # SUCH DAMAGE.
45 import base64
46 import optparse
47 import hashlib
48 import subprocess
49 import sys
50 import unittest
51 from getpass import getpass
54 def parse_args(argv):
55     """Parse command line argments.
56     """
57     usage = 'usage: %prog [options] [string...]'
58     parser = optparse.OptionParser(usage=usage)
59     parser.add_option('-a', '--alphanum', dest='alphanum_mode',
60                       default=False, action='store_true',
61                       help='password includes alphabet and number only')
62     parser.add_option('-c', '--clipboard', dest='clipboard_mode',
63                       default=False, action='store_true',
64                       help='copy password to clipboard (require putclip command)')
65     parser.add_option('-f', '--file', dest='saltfile', metavar='FILE',
66                       help='additional salt file')
67     parser.add_option('-s', '--size', type='int', dest='size',
68                       help='password size')
69     parser.add_option('--test', action='callback', callback=_test,
70                       help='run test and exit')
71     return parser.parse_args(argv)
74 def generate_password(salt, options):
75     """Generate password(hash) from salt.
76     """
77     buf = []
78     for s in salt:
79         buf.extend(':'.encode('utf8'))
80         if isinstance(s, str):
81             buf.extend(s.encode('utf8'))
82         else:
83             buf.extend(s)
84     digest = hashlib.sha1(bytes(buf[1:])).digest()
85     passwd = base64.b64encode(digest).decode('ascii').replace('=', '').strip()
87     if options.alphanum_mode:
88         passwd = passwd.replace('+', '').replace('/', '')
89     if options.size is not None:
90         passwd = passwd[:options.size]
92     return passwd
94 def clipboard_command():
95     """Select clipboard command for platform.
96     """
97     if sys.platform.startswith('linux'):
98         return 'xsel --input --clipboard'
99     elif sys.platform == 'darwin':
100         return 'pbcopy'
101     else:
102         raise Exception('I do not know your clipboard command.')
104 def windows_put_clipboard(string):
105     """Put string to clipboard on Windows.
107     Requires pywin32.
108     """
109     import win32clipboard
110     win32clipboard.OpenClipboard()
111     win32clipboard.SetClipboardText(string)
112     win32clipboard.CloseClipboard()
114 def put_clipboard(string):
115     """Put string to clipboard.
116     """
117     try:
118         windows_put_clipboard(string)
119         return
120     except ImportError:
121         pass
122     if sys.platform == 'cygwin':
123         open('/dev/clipboard', 'wb').write(string)
124         return
125     cmd = clipboard_command()
126     pipe = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE)
127     pipe.stdin.write(string)
128     pipe.stdin.close()
129     pipe.wait()
131 class GeneratePasswordTest(unittest.TestCase):
132     def test_generate(self):
133         """Default.
134         """
135         argv = ['foo', 'bar']
136         options, salt = parse_args(argv)
137         result = generate_password(salt, options)
138         self.assertEqual('VNy+Z9IdXrOUk9Rtia4fQS071t4', result)
140     def test_generate_alpha(self):
141         """Set alpha_num_mode on.
142         """
143         argv = ['foo', 'bar', '-a']
144         options, salt = parse_args(argv)
145         result = generate_password(salt, options)
146         self.assertEqual('VNyZ9IdXrOUk9Rtia4fQS071t4', result)
148     def test_generate_size(self):
149         """Set Size.
150         """
151         argv = ['foo', 'bar', '-s', '6']
152         options, salt = parse_args(argv)
153         result = generate_password(salt, options)
154         self.assertEqual('VNy+Z9', result)
156     def test_generate_alpha_size(self):
157         """Set size and alpha_num_mode.
159         Password size is set size.
160         """
161         argv = ['foo', 'bar', '-a', '-s', '6']
162         options, salt = parse_args(argv)
163         result = generate_password(salt, options)
164         self.assertEqual('VNyZ9I', result)
167 def _test(option, opt_str, value, parser, *args, **kwargs):
168     suite = unittest.TestSuite()
169     suite.addTest(unittest.makeSuite(GeneratePasswordTest))
170     result = unittest.TextTestRunner(verbosity=2).run(suite)
171     if result.errors or result.failures:
172         sys.exit(1)
173     else:
174         sys.exit()
177 def main():
178     options, salt = parse_args(sys.argv[1:])
179     if options.saltfile:
180         salt.append(open(options.saltfile, 'rb').read())
181     passphrase = getpass('Passphrase:')
182     salt.append(passphrase)
183     passwd = generate_password(salt, options)
184     if options.clipboard_mode:
185         print('put password to clipboard.')
186         put_clipboard(passwd)
187     else:
188         print(passwd)
191 if __name__ == '__main__':
192     main()