|
@@ -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
|
|
|
+"""
|