2 Commits 43fa5bd613 ... c61128f54c

Author SHA1 Message Date
  Pat Beirne c61128f54c changed bit ifelse() chain to a match/case 4 months ago
  Pat Beirne 2bbe080275 complete overhaul, version 1.3 4 months ago
2 changed files with 254 additions and 116 deletions
  1. 18 19
      README.md
  2. 236 97
      pwgen.py

+ 18 - 19
README.md

@@ -1,16 +1,16 @@
 # pwgen
 
-Generate word-based passwords. Based on XKCD/936, word-based passwords are easier to remember than a string of random letters/numbers/punctuation.
+Generate word-based passwords. Based on [XKCD/936](https://xkcd.com/936/), word-based passwords are easier to remember than a string of random letters/numbers/punctuation.
 
 [TOC]
 
 ### Overview
 
-Password generators can be found all over the internet, but most generate random combinations of letters+numbers+punctuation. If a combination of those is long enough to be secure, it is also long enough to be difficult to remember, and possible difficult to type.
+Password generators can be found all over the internet, but most generate random combinations of letters + numbers + punctuation. If a combination of those is long enough to be secure, it is also long enough to be difficult to remember, and possible difficult to type.
 
-The XKCD comic #936 introduced the idea that a phrase of 3, 4 or 5 common words can be used as a password that is sufficiently difficult to crack in our lifetimes. The program **pwgen** is a locally hosted generator of these kinds of pass phrases.
+The XKCD comic #936 introduced the idea that a phrase of 3, 4 or 5 common words can be used as a password that is sufficiently difficult to crack in our lifetimes. The program `**pwgen**` is a locally hosted generator of these kinds of pass phrases.
 
-The program runs locally on a UNIX/Linux based computer, and the passphrase never crosses the network, so you can be sure that you're the only one who has ever seen it.
+This program runs locally on a UNIX/Linux based computer, and the passphrase never crosses the network, so you can be sure that you're the only one who has ever seen it.
 
 ### Usage
 
@@ -27,40 +27,39 @@ as a potential pass phrase. The following options exist:
     -p    pad the words with spaces
     -P    pad the words with spaces, periods or commas
     -n    pad with numbers
-    -N    pad with numbers, spaces, periods or commas
-    -L <n>
-    --length <n> minimum phrase length (default=20)
+    -N    pad with numbers and punctuation (other than space)
+    -L <n>  minimum phrase length (default=20) (or --length <n>)
     -c    make 2 word phrases with conjunction between them
     <n>   make multiple phrases (default=1)
 
-By default, <code>pwgen</code> will build a pass phrase using words from the dictionary at <code>/usr/share/dict/words</code> and add punctuation to separate the words. 
+By default, **`pwgen`** will build a pass phrase using words from the dictionary at **`/usr/share/dict/words`** and add punctuation to separate the words. 
 
     $ pwgen
     consequently?restraining
 
-Adding the **<code>-p</code>** option will insist that the passphrase be padded with only spaces, making it easier to type on a smartphone. Similarly, the **<code>-P</code>** option uses only spaces, periods and commas, which are often easily accessed on a smartphone.
+Adding the **`-p`** option will insist that the passphrase be padded with only spaces, making it easier to type on a smartphone. Similarly, the **`-P`** option uses only spaces, periods and commas, which are often easily accessed on a smartphone.
 
     $ pwgen -p
     garments mazes queasy
     $ pwgen -P
     crufted..teeth...buy
 
-Some servers require numbers to be embedded in the password, so the **<code>-n</code>** and **<code>-N</code>** options are available.
+Some servers require numbers to be embedded in the password, so the **`-n`** and **`-N`** options are available.
 
     $ pwgen -N
-    bleeding5clusters5berries
+    bleeding5clusters5berries#
 
-The **<code>-L</code>** option allows you to specify a minimum length for the pass phrase, although the result may be a few characters longer than you requested, so that the words aren't chopped up.
+The **`-L`** option allows you to specify a minimum length for the pass phrase, although the result may be a few characters longer than you requested, so that the words aren't chopped up.
 
     $ pwgen -L 30
     dittoed___nonabrasive___atomic
 
-The **<code>-c</code>** option uses common English conjunctions to separate a pair of words. If the 2nd word starts with a vowel, the conjuntion "a" is changed to "an". I find these phrases much easier to remember than just a sequence of words.
+The **`-c`** option uses common English conjunctions to separate a pair of words. If the 2nd word starts with a vowel, the conjuntion "a" is changed to "an". I find these phrases much easier to remember than just a sequence of words.
 
     $ pwgen -c
     quick in a crowd
 
-Part of the reason for using pass phrases is to make them easier to remember for us humans. Sometimes, the first phrases offered just doesn't seem memorable, so you can add a small integer after <code>pwgen</code> to get multiple offerings, and you can just choose the one that seems memorable to you.
+Part of the reason for using pass phrases is to make them easier to remember for us humans. Sometimes, the first phrases offered just doesn't seem memorable, so you can add a small integer after `pwgen` to get multiple offerings, and you can just choose the one that seems memorable to you.
 
     $ pwgen 6
     mechanisms..contempt
@@ -76,12 +75,12 @@ Coming soon: Instead of spaces & punctuation, you will be able to use CamelCase
 
 #### Other implementations
 
-http://www.egansoft.com/password/index.php
+<http://www.egansoft.com/password/index.php>
 
-https://passwords-generator.org/words
+<https://passwords-generator.org/words>
 
-https://passwordcreator.org/commonwords.html
+<https://passwordcreator.org/commonwords.html>
 
-https://mdigi.tools/memorable-password/
+<https://mdigi.tools/memorable-password/>
 
-https://www.mapletech.co.uk/tools/password-generator/ 
+<https://www.mapletech.co.uk/tools/password-generator/> 

+ 236 - 97
pwgen.py

@@ -1,109 +1,208 @@
-#!/usr/bin/python
+#!/usr/bin/python3
 
 """generate word compound passwords 
 
 usage: pwgen -c [number_of_phrases default=1]
-or     pwgen [len default=20] [number_of_phrases default=1]    // no conjunctions
-	-c   use conjunction
+or     pwgen [-s | -S | -n | -p | -/] [-L len default=20] [number_of_phrases default=1]  
+
+
 """
+
 import random, sys
+from enum import Enum
+
+class Padding(Enum):
+  spaces = 0
+  dots = 1
+  punct = 2
+  numbers = 3
+  camel = 4
+  snake = 5
+
+class Config:
+  def __init__(self):
+    self.padding = Padding.spaces
+    self.first_punct  = False
+    self.last_punct   = False
+    self.first_number = False
+    self.last_number  = False
+    self.capitalize   = False
+    self.use_conjunctions = False
+    self.target_len = 20    # default pw len
+    self.number    = 1
+  def __repr__(self):
+    return(str(list((repr(self.padding),
+      self.first_punct, self.last_punct,
+      self.first_number, self.last_number,
+      self.capitalize,
+      self.target_len, self.number,
+      self.use_conjunctions))))
+ 
+SPACE        = " "
+DOTS         = ".,"
+PUNCTS       = """ !@#$%^&*()-_=+[{]}\\|;:/?.>,<~"""
+NUMBERS      = "0123456789"
+CONJUNCTIONS =  ["and", "and a", "and the","or", "or the", "or a", 
+    "with a", "with", "with the", "by a", "by the", 
+    "on a", "on the","on", "in a", "in the", "in", 
+    "for", "for a", "for the"]
+
+# delete these
+number = 1
+targetLen = 20
 
-targetLen = 20		# default pw len
 useConjunction = False
-number = 1
-punc = " !@#$%^&*()-_=+[{]}\|;:/?.>,<`~"
-number_punc = "0123456789"
+add_punc       = False
+add_number     = False
 
 def toInt(s, chkRange, errStr = "sorry, that's not a number", rangeStr = "sorry, I can't do that number"):
-	try:
-		t = int(s)
-	except:
-		print errStr
-		sys.exit()
-	if t<chkRange[0] or t>chkRange[1]:
-		print rangeStr
-		sys.exit()
-	return t
-
-narg = 1
-if len(sys.argv)>1:
-	if sys.argv[1]=="-h" or sys.argv[1]=="--help":
-		print "pwgen.py  v1.3  generate passphrase; inspired by xkcd/936 and SteveGibson\n"
-		print "Usage: pwgen.py [-p | -P | -n | -N] [length [num_phrases]] "
-		print "       pwgen.py -c [num_phrases]"
-		print "       pwgen.py -h | --help \n"
-		print "     -p           pad with only spaces (for smartphone)"
-		print "     -P           pad with spaces, period and comma (for smartphone)"
-		print "     -n           pad with numbers"
-		print "     -N           pad with numbers, spaces, period, comma like -P -n"
-		print "     length       make pass phrases padded with punctuation filler; default=20"
-		print "     -c           make 2 word pass phrases with a conjuction filler"
-		print "     num_phrases  make multiple pass phrases, one per line; default=1\n"
-		sys.exit()
-	elif sys.argv[1]=="-c":	# conjunction mode
-		useConjunction = True
-		narg = narg + 1
-	elif sys.argv[1]=='-n': # number fill mode
-		punc = number_punc
-		narg = narg + 1
-	elif sys.argv[1]=='-N':
-		punc = number_punc + " ,."
-		narg = narg + 1
-	elif sys.argv[1]=="-p": # use only space
-		punc = " "
-		narg = narg + 1
-	elif sys.argv[1]=="-P":
-		punc = " .,"
-		narg = narg + 1
-	if not useConjunction:
-		if len(sys.argv)>narg:
-			targetLen = toInt(sys.argv[narg],[12,36],rangeStr = "Sorry, I can't build phrases that long")
-			narg = narg + 1
-	if len(sys.argv)>narg:
-		number = toInt(sys.argv[narg],[0,40])  # pick off the number of phrases
- 
-f = open("/usr/share/dict/words")
-#f = open("/usr/share/dict/corncob_lowercase.txt")
-d = f.read().splitlines()
-f.close()
-
-numWords = len(d)	
-
-for i in range(number):
-
-	w=['','','']
-	for i in range(3):
-		w[i] = d[random.randint(0,numWords-1)]
-		while (len(w[i]) > 10) or (w[i].endswith("'s")):
-			w[i] = d[random.randint(0,numWords-1)]
-			
-	whole = len(w[0])+len(w[1])
-
-	thisPunct = punc[int(random.random() * len(punc))] 
-
-	conjunctions = ["and", "and a", "and the","or", "or the", "or a", 
-		"with a", "with", "with the", "by a", "by the", 
-		"on a", "on the","on", "in a", "in the", "in", 
-		"for", "for a", "for the"]
-
-	if useConjunction == False:
-		if whole + 6 >= targetLen:	# if 2 words is enough
-			r = max(1,targetLen - whole)
-			pw = w[0] + thisPunct*r + w[1]
-		else:		# otherwise use 3
-			whole = whole + len(w[2])
-			r = targetLen - whole
-			if r<2:
-				r = 2
-			pw = w[0] + thisPunct*(r/2) + w[1] + thisPunct*(r-r/2) + w[2]
-	else:
-		conj = conjunctions[random.randint(0,len(conjunctions)-1)]
-		if w[1][0].lower() in ['a','A','e','E','i','I','o','O','u','U']:
-			if conj[-2:]==" a":
-				conj = conj+'n'
-		pw = w[0] + ' ' + conj + ' ' + w[1]
-
-	print pw
+  try:
+    t = int(s)
+  except:
+    print (errStr)
+    sys.exit()
+  if t<chkRange[0] or t>chkRange[1]:
+    print (rangeStr)
+    sys.exit()
+  return t
+
+def get_args(conf):
+  narg = 1
+  nargs = len(sys.argv)
+  while narg<nargs:
+    a = sys.argv[narg]
+    if a=="-h" or a=="--help" or a=="--version":
+      print ("pwgen.py  v1.3  generate passphrase; inspired by xkcd/936 and SteveGibson\n")
+      print ("""Usage: pwgen.py [-sSpPkKnNcCaMX] [-L length] [num_phrases] 
+         pwgen.py -h | --help | --version
+
+       -s           pad with only spaces (default; for smartphone)
+       -S           pad with period and comma (for smartphone)
+       -p           pad with special characters
+       -P           pad with numbers
+       -k           connect the words as CamelCase
+       -K           connect the words as snake_case
+       -n           add a number at the beginning
+       -N           add a number at the end
+       -c           add a special character at the begnning
+       -C           add a special character at the end
+       -M           capitalize the words
+       -a           connect the words with a conjuction filler
+       -X           same as -MNCS (capital, periods & commas, extra number & char)
+       -L <n>       make pass phrases at least this long; default=20
+       num_phrases  make multiple pass phrases, one per line; default=1\n""")
+      sys.exit()
+    elif a[0]=='-':
+      for b in a[1:]:
+        match b:
+          case 'c':
+            conf.first_punt = True
+          case 'C':
+            conf.last_punct = True
+          case 'n':
+            conf.first_number = True
+          case 'N':  # add number
+            conf.last_number = True
+          case 's':
+            conf.padding = Padding.spaces
+          case 'S':
+            conf.padding = Padding.dots
+          case 'p':
+            conf.padding = Padding.punct
+          case 'P':
+            conf.padding = Padding.numbers
+          case 'k':
+            conf.padding = Padding.camel
+          case 'K':
+            conf.padding = Padding.snake
+          case 'M': 
+            conf.capitalize = True  
+          case 'X':
+            conf.capitalize = True
+            conf.last_punct = True
+            conf.last_number = True
+            conf.padding = Padding.dots
+          case 'a':
+            conf.use_conjunctions = True;
+          case 'L':
+            narg = narg + 1
+            if narg < nargs:
+              conf.target_len = toInt(sys.argv[narg],[12,66],rangeStr = "Sorry, I can't build phrases that long")
+          case _:
+            print(f">>> unexpected command option '{b}'")      
+    else:
+      if narg < nargs:
+        conf.number = toInt(sys.argv[narg],[0,40],rangeStr = "Sorry, I can't print that many; try a smaller number")  # pick off the number of phrases
+
+    narg = narg + 1
+
+
+def load_words():
+  f = open("/usr/share/dict/words")
+  #f = open("/usr/share/dict/corncob_lowercase.txt")
+  d = f.read().splitlines()
+  f.close()
+  d = list(filter(lambda x : len(x)<10 and not x.endswith("'s"), d))
+  return d
+
+def get_word(conf, data):
+  m = data[random.randint(0,len(data)-1)]
+  if conf.capitalize or conf.padding == Padding.camel:
+    m = m.capitalize()
+  return m
+
+def pwgen(conf, data):
+  w = [get_word(conf, data)]
+  nExtraChars = conf.first_punct+conf.last_punct+conf.first_number+conf.last_number
+  # TODO: we need a length estimator for this loop
+  while sum(len(a) for a in w) + (len(w)-1 if not conf.padding==Padding.camel else 0) + nExtraChars < conf.target_len:
+    if conf.use_conjunctions == True:
+      c = CONJUNCTIONS[random.randint(0,len(CONJUNCTIONS)-1)]
+      w.extend(c.split())
+    w.append(get_word(conf, data))    
+    if w[-2] == 'a' and w[-1][0] in "aeiouAEIOU":
+      w[-2] = "an"
+  # change so that two-word conjunctions are multiple list elements
+  
+  if conf.padding == Padding.camel:
+    ret = "".join(w)
+  elif conf.padding == Padding.spaces:
+    ret = SPACE.join(w)
+  elif conf.padding == Padding.dots:
+    p = DOTS[random.randint(0,len(DOTS)-1)]
+    ret = p.join(w)
+  elif conf.padding == Padding.numbers:
+    n = NUMBERS[random.randint(0,len(NUMBERS)-1)]
+    ret = n.join(w)
+  elif conf.padding == Padding.punct:
+    p = PUNCTS[random.randint(0,len(PUNCTS)-1)]
+    ret = p.join(w)
+  elif conf.padding == Padding.snake:
+    ret = '_'.join(w)  
+
+  n = NUMBERS[random.randint(0,len(NUMBERS)-1)]
+  if conf.first_number:
+    ret = n + ret
+  if conf.last_number:
+    ret = ret + n    
+  p = PUNCTS[random.randint(0,len(PUNCTS)-1)]
+  if conf.first_punct:
+    ret = p + ret
+  if conf.last_punct:
+    ret = ret + p    
+
+  return ret
+
+if __name__ == "__main__":
+
+  conf = Config()
+  get_args(conf)
+
+  data = load_words()
+
+  for i in range(conf.number):
+    pw = pwgen(conf, data)
+    print(pw)
 
 
 """ 
@@ -112,5 +211,45 @@ although this
 read n; i="1"; while [ $i -le $n ]; do cat /etc/dictionaries-common/words | shuf | head -1; i=$(( $i + 1 )); done
 
 will do the job almost as well
-"""
 
+NEW PLAN
+  -s spaces (default)
+  -S comma+period
+  -p punctuation
+  -P number 
+  -k CamelCase 
+  -K SnakeCase (only one of these 6 allowed)
+
+  -c special char at beginning
+  -C special char at end
+  -n number at beginning
+  -N number at end
+  -M capitalize all words
+  -a fill with conjunctions
+  -X equivalent to -SCNM
+  -L <n> length
+  <n> number of samples
+  -k -a not recommended
+
+  -s    dog cat mouse
+  -S    dog.cat.mouse
+  -p    dog#cat#mouse
+  -P    dog4cat4mouse
+  -c    !dog cat mouse
+  -C    dog cat mouse#
+  -n    4dog cat mouse
+  -N    dog cat mouse4
+  -cnCN $4dog cat mouse4$
+  -M    Dog Cat Mouse
+  -k    DogCatMouse
+  -kN   DogCatMouse2
+  -K    dog_cat_mouse
+  -KMN  Dog_Cat_Mouse3
+  -a    dog and a cat with mouse
+  -Sa   dog.and.a.cat.with.mouse
+  -pa   dog#and#a#cat#with#mouse
+  -paCN dog^and^a^cat^with^mouse5%
+  -X    Dog.Cat.Mouse6&
+
+leet encoder a->4A e->3 i->I o->0 s->5$ t->7 
+"""