#!/usr/pkg/bin/python2.7
# PolicyGen - Analyze and Generate password masks according to a password policy
#
# This tool is part of PACK (Password Analysis and Cracking Kit)
#
# VERSION 0.0.2
#
# Copyright (C) 2013 Peter Kacherginsky
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met: 
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer. 
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution. 
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import string, random
from optparse import OptionParser, OptionGroup
import itertools

VERSION = "0.0.1"

# PPS (Passwords per Second) Cracking Speed
pps = 1000000000

# Global Variables
sample_time = 0
total_time = 0

##################################################################
# Calculate complexity of a single mask
##################################################################
def complexity(mask):
	count = 1
	for char in mask[1:].split("?"):
		if   char == "l": count *= 26
		elif char == "u": count *= 26
		elif char == "d": count *= 10
		elif char == "s": count *= 33
		else: "[!] Error, unknown mask ?%s" % char

	return count

###################################################################
# Check whether a sample password mask matches defined policy
###################################################################       
def filtermask(maskstring,options):
	global total_time, sample_time
	
	# define character counters
	lowercount = uppercount = digitcount = specialcount = 0
	
	# calculate password complexity and cracking time
	mask_time = complexity(maskstring)/options.pps
	total_time += mask_time
	
	for char in maskstring[1:].split("?"):
		if char == "l": lowercount += 1
		elif char == "u": uppercount += 1
		elif char == "d": digitcount += 1
		elif char == "s": specialcount += 1
			
	# Filter according to password policy
	if lowercount   >= options.minlower   and lowercount   <= options.maxlower and \
	   uppercount   >= options.minupper   and uppercount   <= options.maxupper and \
	   digitcount   >= options.mindigits  and digitcount   <= options.maxdigits and \
	   specialcount >= options.minspecial and specialcount <= options.maxspecial:
		sample_time += mask_time
		if options.verbose:
			print "[*] [%dd|%dh|%dm|%ds] %s [l:%d u:%d d:%d s:%d]" % (mask_time/60/60/24, mask_time/60/60, mask_time/60, mask_time,maskstring,lowercount,uppercount,digitcount,specialcount)
		return True
	else:
		return False


def main():
	# define mask counters
	total_count = sample_count = 0

	header  = "                       _ \n"
	header += "     PolicyGen 0.0.1  | |\n"  
	header += "      _ __   __ _  ___| | _\n"
	header += "     | '_ \ / _` |/ __| |/ /\n"
	header += "     | |_) | (_| | (__|   < \n"
	header += "     | .__/ \__,_|\___|_|\_\\\n"
	header += "     | |                    \n"
	header += "     |_| iphelix@thesprawl.org\n"
	header += "\n"

	# parse command line arguments
	parser = OptionParser("%prog [options]\n\nType --help for more options", version="%prog "+VERSION)
	parser.add_option("--length", dest="length", help="Password length", type="int", default=8, metavar="8")
	parser.add_option("-o", "--output", dest="output",help="Save masks to a file", metavar="masks.txt")
	parser.add_option("--pps", dest="pps", help="Passwords per Second", type="int", default=pps, metavar="1000000000")
	parser.add_option("-v", "--verbose", action="store_true", dest="verbose")

	group = OptionGroup(parser, "Password Policy", "Define the minimum (or maximum) password strength policy that you would like to test")
	group.add_option("--mindigits", dest="mindigits", help="Minimum number of digits", default=0, type="int", metavar="1")
	group.add_option("--minlower", dest="minlower", help="Minimum number of lower-case characters", default=0, type="int", metavar="1")
	group.add_option("--minupper", dest="minupper", help="Minimum number of upper-case characters", default=0, type="int", metavar="1")
	group.add_option("--minspecial", dest="minspecial", help="Minimum number of special characters", default=0, type="int", metavar="1")
	group.add_option("--maxdigits", dest="maxdigits", help="Maximum number of digits", default=9999, type="int", metavar="3")
	group.add_option("--maxlower", dest="maxlower", help="Maximum number of lower-case characters", default=9999, type="int", metavar="3")
	group.add_option("--maxupper", dest="maxupper", help="Maximum number of upper-case characters", default=9999, type="int", metavar="3")
	group.add_option("--maxspecial", dest="maxspecial", help="Maximum number of special characters", default=9999, type="int", metavar="3")
	parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.")
	parser.add_option_group(group)

	(options, args) = parser.parse_args()

	# cleanup maximum occurence options
	if options.maxlower > options.length: options.maxlower = options.length
	if options.maxdigits > options.length: options.maxdigits = options.length
	if options.mindigits > options.length: options.mindigits = options.length
	if options.maxupper > options.length: options.maxupper = options.length
	if options.maxspecial > options.length: options.maxspecial = options.length

	# Print program header
	if not options.quiet:
		print header

	# print current password policy
	print "[*] Password policy:"
	print "[+] Password length: %d" % options.length
	print "[+] Minimum strength: lower: %d, upper: %d, digits: %d, special: %d" % (options.minlower, options.minupper, options.mindigits, options.minspecial)
	print "[+] Maximum strength: lower: %d, upper: %d, digits: %d, special: %d" % (options.maxlower, options.maxupper, options.maxdigits, options.maxspecial)

	if options.output: f = open(options.output, 'w')

	# generate all possible password masks and compare them to policy
	# TODO: Randomize or even statistically arrange matching masks
	for password in itertools.product(['?l','?u','?d','?s'],repeat=options.length):
		if filtermask(''.join(password), options):
		    if options.output: f.write("%s\n" % ''.join(password))
		    sample_count +=1
		total_count += 1

	if options.output: f.close()

	print "[*] Total Masks:  %d Runtime: [%dd|%dh|%dm|%ds]" % (total_count, total_time/60/60/24, total_time/60/60, total_time/60, total_time)
	print "[*] Policy Masks: %d Runtime: [%dd|%dh|%dm|%ds]" % (sample_count, sample_time/60/60/24, sample_time/60/60, sample_time/60, sample_time)

if __name__ == "__main__":
	main()
