PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/pass_phrase.py

https://github.com/aaronbassett/Pass-phrase
Python | 419 lines | 415 code | 2 blank | 2 comment | 3 complexity | f920f2bd4e5ef0c4ad282f6010632996 MD5 | raw file
  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. import random
  4. import optparse
  5. import sys
  6. import re
  7. import os
  8. import math
  9. import datetime
  10. import string
  11. __LICENSE__ = """
  12. The MIT License (MIT)
  13. Copyright (c) 2012 Aaron Bassett, http://aaronbassett.com
  14. Permission is hereby granted, free of charge, to any person
  15. obtaining a copy of this software and associated documentation
  16. files (the "Software"), to deal in the Software without restriction,
  17. including without limitation the rights to use, copy, modify,
  18. merge, publish, distribute, sublicense, and/or sell copies of the
  19. Software, and to permit persons to whom the Software is furnished
  20. to do so, subject to the following conditions:
  21. The above copyright notice and this permission notice shall be
  22. included in all copies or substantial portions of the Software.
  23. THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
  24. EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  25. OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  26. NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  27. HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  28. IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
  29. IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  30. """
  31. # random.SystemRandom() should be cryptographically secure
  32. try:
  33. rng = random.SystemRandom
  34. except AttributeError:
  35. sys.stderr.write("WARNING: System does not support cryptographically "
  36. "secure random number generator or you are using Python "
  37. "version < 2.4.\n"
  38. "Continuing with less-secure generator.\n")
  39. rng = random.Random
  40. # Python 3 compatibility
  41. if sys.version[0] == "3":
  42. raw_input = input
  43. def validate_options(options, args):
  44. """
  45. Given a set of command line options, performs various validation checks
  46. """
  47. if options.num <= 0:
  48. sys.stderr.write("Little point running the script if you "
  49. "don't generate even a single passphrase.\n")
  50. sys.exit(1)
  51. if options.max_length < options.min_length:
  52. sys.stderr.write("The maximum length of a word can not be "
  53. "lesser then minimum length.\n"
  54. "Check the specified settings.\n")
  55. sys.exit(1)
  56. if len(args) >= 1:
  57. parser.error("Too many arguments.")
  58. for word_type in ["adjectives", "nouns", "verbs"]:
  59. wordfile = getattr(options, word_type, None)
  60. if wordfile is not None:
  61. if not os.path.exists(os.path.abspath(wordfile)):
  62. sys.stderr.write("Could not open the specified {0} word file.\n".format(word_type))
  63. sys.exit(1)
  64. else:
  65. common_word_file_locations = ["{0}.txt", "~/.pass-phrase/{0}.txt"]
  66. for loc in common_word_file_locations:
  67. wordfile = loc.format(word_type)
  68. if os.path.exists(wordfile):
  69. setattr(options, word_type, wordfile)
  70. break
  71. if getattr(options, word_type, None) is None:
  72. sys.stderr.write("Could not find {0} word file, or word file does not exist.\n".format(word_type))
  73. sys.exit(1)
  74. def leet(word):
  75. geek_letters = {
  76. "a": ["4", "@"],
  77. "b": ["8",],
  78. "c": ["(",],
  79. "e": ["3",],
  80. "f": ["ph", "pH"],
  81. "g": ["9", "6"],
  82. "h": ["#",],
  83. "i": ["1", "!", "|"],
  84. "l": ["!", "|"],
  85. "o": ["0", "()"],
  86. "q": ["kw",],
  87. "s": ["5", "$"],
  88. "t": ["7",],
  89. "x": ["><",],
  90. "y": ["j",],
  91. "z": ["2",]
  92. }
  93. geek_word = ""
  94. for letter in word:
  95. l = letter.lower()
  96. if l in geek_letters:
  97. # swap out the letter *most* (80%) of the time
  98. if rng().randint(1,5) % 5 != 0:
  99. letter = rng().choice(geek_letters[l])
  100. else:
  101. # uppercase it *some* (10%) of the time
  102. if rng().randint(1,10) % 10 != 0:
  103. letter = letter.upper()
  104. geek_word += letter
  105. # if last letter is an S swap it out half the time
  106. if word[-1:].lower() == "s" and rng().randint(1,2) % 2 == 0:
  107. geek_word = geek_word[:-1] + "zz"
  108. return geek_word
  109. def mini_leet(word):
  110. geek_letters = {
  111. "a": "4",
  112. "b": "8",
  113. "e": "3",
  114. "g": "6",
  115. "i": "1",
  116. "o": "0",
  117. "s": "5",
  118. "t": "7",
  119. "z": "2",
  120. }
  121. geek_word = ""
  122. for letter in word:
  123. l = letter.lower()
  124. if l in geek_letters:
  125. letter = geek_letters[l]
  126. geek_word += letter
  127. return geek_word
  128. def generate_wordlist(wordfile=None,
  129. min_length=0,
  130. max_length=20,
  131. valid_chars='.',
  132. make_leet=False,
  133. make_mini_leet=False):
  134. """
  135. Generate a word list from either a kwarg wordfile, or a system default
  136. valid_chars is a regular expression match condition (default - all chars)
  137. """
  138. words = []
  139. regexp = re.compile("^%s{%i,%i}$" % (valid_chars, min_length, max_length))
  140. # At this point wordfile is set
  141. wordfile = os.path.expanduser(wordfile) # just to be sure
  142. wlf = open(wordfile)
  143. for line in wlf:
  144. thisword = line.strip()
  145. if regexp.match(thisword) is not None:
  146. if make_mini_leet:
  147. thisword = mini_leet(thisword)
  148. elif make_leet:
  149. thisword = leet(thisword)
  150. words.append(thisword)
  151. wlf.close()
  152. if len(words) < 1:
  153. sys.stderr.write("Could not get enough words!\n")
  154. sys.stderr.write("This could be a result of either {0} being too small,\n".format(wordfile))
  155. sys.stderr.write("or your settings too strict.\n")
  156. sys.exit(1)
  157. return words
  158. def craking_time(seconds):
  159. minute = 60
  160. hour = minute * 60
  161. day = hour * 24
  162. week = day * 7
  163. if seconds < 60:
  164. return "less than a minute"
  165. elif seconds < 60 * 5:
  166. return "less than 5 minutes"
  167. elif seconds < 60 * 10:
  168. return "less than 10 minutes"
  169. elif seconds < 60 * 60:
  170. return "less than an hour"
  171. elif seconds < 60 * 60 * 24:
  172. hours, r = divmod(seconds, 60 * 60)
  173. return "about %i hours" % hours
  174. elif seconds < 60 * 60 * 24 * 14:
  175. days, r = divmod(seconds, 60 * 60 * 24)
  176. return "about %i days" % days
  177. elif seconds < 60 * 60 * 24 * 7 * 8:
  178. weeks, r = divmod(seconds, 60 * 60 * 24 * 7)
  179. return "about %i weeks" % weeks
  180. elif seconds < 60 * 60 * 24 * 365 * 2:
  181. months, r = divmod(seconds, 60 * 60 * 24 * 7 * 4)
  182. return "about %i months" % months
  183. else:
  184. years, r = divmod(seconds, 60 * 60 * 24 * 365)
  185. return "about %i years" % years
  186. def verbose_reports(**kwargs):
  187. """
  188. Report entropy metrics based on word list size"
  189. """
  190. options = kwargs.pop("options")
  191. f = {}
  192. for word_type in ["adjectives", "nouns", "verbs"]:
  193. print("The supplied {word_type} list is located at {loc}.".format(
  194. word_type=word_type,
  195. loc=os.path.abspath(getattr(options, word_type))
  196. ))
  197. words = kwargs[word_type]
  198. f[word_type] = {}
  199. f[word_type]["length"] = len(words)
  200. f[word_type]["bits"] = math.log(f[word_type]["length"], 2)
  201. if (int(f[word_type]["bits"]) == f[word_type]["bits"]):
  202. print("Your %s word list contains %i words, or 2^%i words."
  203. % (word_type, f[word_type]["length"], f[word_type]["bits"]))
  204. else:
  205. print("Your %s word list contains %i words, or 2^%0.2f words."
  206. % (word_type, f[word_type]["length"], f[word_type]["bits"]))
  207. entropy = f["adjectives"]["bits"] +\
  208. f["nouns"]["bits"] +\
  209. f["verbs"]["bits"] +\
  210. f["adjectives"]["bits"] +\
  211. f["nouns"]["bits"]
  212. print("A passphrase from this list will have roughly "
  213. "%i (%0.2f + %0.2f + %0.2f + %0.2f + %0.2f) bits of entropy, " % (
  214. entropy,
  215. f["adjectives"]["bits"],
  216. f["nouns"]["bits"],
  217. f["verbs"]["bits"],
  218. f["adjectives"]["bits"],
  219. f["nouns"]["bits"]
  220. ))
  221. combinations = math.pow(2, int(entropy)) / 1000
  222. time_taken = craking_time(combinations)
  223. print "Estimated time to crack this passphrase (at 1,000 guesses per second): %s\n" % time_taken
  224. def generate_passphrase(adjectives, nouns, verbs, separator):
  225. return "{0}{s}{1}{s}{2}{s}{3}{s}{4}".format(
  226. rng().choice(adjectives),
  227. rng().choice(nouns),
  228. rng().choice(verbs),
  229. rng().choice(adjectives),
  230. rng().choice(nouns),
  231. s=separator
  232. )
  233. def passphrase(adjectives, nouns, verbs, separator, num=1,
  234. uppercase=False, lowercase=False, capitalise=False):
  235. """
  236. Returns a random pass-phrase made up of
  237. adjective noun verb adjective noun
  238. I find this basic structure easier to
  239. remember than XKCD style purely random words
  240. """
  241. phrases = []
  242. for i in range(0, num):
  243. phrase = generate_passphrase(adjectives, nouns, verbs, separator)
  244. if capitalise:
  245. phrase = string.capwords(phrase)
  246. phrases.append(phrase)
  247. all_phrases = "\n".join(phrases)
  248. if uppercase:
  249. all_phrases = all_phrases.upper()
  250. elif lowercase:
  251. all_phrases = all_phrases.lower()
  252. return all_phrases
  253. if __name__ == "__main__":
  254. usage = "usage: %prog [options]"
  255. parser = optparse.OptionParser(usage)
  256. parser.add_option("--adjectives", dest="adjectives",
  257. default=None,
  258. help="List of valid adjectives for passphrase")
  259. parser.add_option("--nouns", dest="nouns",
  260. default=None,
  261. help="List of valid nouns for passphrase")
  262. parser.add_option("--verbs", dest="verbs",
  263. default=None,
  264. help="List of valid verbs for passphrase")
  265. parser.add_option("-s", "--separator", dest="separator",
  266. default=' ',
  267. help="Separator to add between words")
  268. parser.add_option("-n", "--num", dest="num",
  269. default=1, type="int",
  270. help="Number of passphrases to generate")
  271. parser.add_option("--min", dest="min_length",
  272. default=0, type="int",
  273. help="Minimum length of a valid word to use in passphrase")
  274. parser.add_option("--max", dest="max_length",
  275. default=20, type="int",
  276. help="Maximum length of a valid word to use in passphrase")
  277. parser.add_option("--valid_chars", dest="valid_chars",
  278. default='.',
  279. help="Valid chars, using regexp style (e.g. '[a-z]')")
  280. parser.add_option("-U", "--uppercase", dest="uppercase",
  281. default=False, action="store_true",
  282. help="Force passphrase into uppercase")
  283. parser.add_option("-L", "--lowercase", dest="lowercase",
  284. default=False, action="store_true",
  285. help="Force passphrase into lowercase")
  286. parser.add_option("-C", "--capitalise", "--capitalize", dest="capitalise",
  287. default=False, action="store_true",
  288. help="Force passphrase to capitalise each word")
  289. parser.add_option("--l337", dest="make_leet",
  290. default=False, action="store_true",
  291. help="7#izz R3@l|j !$ 4941Nst 7#3 w#()|e 5P|R!7 0pH t#3 7#|N6.")
  292. parser.add_option("--l337ish", dest="make_mini_leet",
  293. default=False, action="store_true",
  294. help="A l337 version which is easier to remember.")
  295. parser.add_option("-V", "--verbose", dest="verbose",
  296. default=False, action="store_true",
  297. help="Report various metrics for given options")
  298. (options, args) = parser.parse_args()
  299. validate_options(options, args)
  300. # Generate word lists
  301. adjectives = generate_wordlist(wordfile=options.adjectives,
  302. min_length=options.min_length,
  303. max_length=options.max_length,
  304. valid_chars=options.valid_chars,
  305. make_mini_leet=options.make_mini_leet,
  306. make_leet=options.make_leet)
  307. nouns = generate_wordlist(wordfile=options.nouns,
  308. min_length=options.min_length,
  309. max_length=options.max_length,
  310. valid_chars=options.valid_chars,
  311. make_mini_leet=options.make_mini_leet,
  312. make_leet=options.make_leet)
  313. verbs = generate_wordlist(wordfile=options.verbs,
  314. min_length=options.min_length,
  315. max_length=options.max_length,
  316. valid_chars=options.valid_chars,
  317. make_mini_leet=options.make_mini_leet,
  318. make_leet=options.make_leet)
  319. if options.verbose:
  320. verbose_reports(adjectives=adjectives,
  321. nouns=nouns,
  322. verbs=verbs,
  323. options=options)
  324. print(passphrase(
  325. adjectives,
  326. nouns,
  327. verbs,
  328. options.separator,
  329. num=int(options.num),
  330. uppercase=options.uppercase,
  331. lowercase=options.lowercase,
  332. capitalise=options.capitalise
  333. )
  334. )