#!/usr/local/bin/python3.sh
# -*-mode: python; indent-tabs-mode: nil; py-indent-offset: 4; coding: utf-8 -*-
# https://github.com/reid-k/gridfire

from __future__ import unicode_literals, print_function

ROLE="proxy" # noqa
__program__ = "Gridfire"
__version__ = "v0.5+testforge.21.11"
__doc__ = """
Gridfire is inspired by gpggrid, a security tool included in

Tinfoil Hat Linux, intended to resist shoulder-surfing and keylogging.
For more information on the project, see the README file distributed with Gridfire.

Gridfire is named after a fictional superweapon in Iain M Banks' Culture novels.
See http://everything2.com/title/Gridfire for more.
RIP Iain M Banks (1954-02-16 to 2013-06-09)

usage: gridfire.py [-h] [-a TTYALERT] [-c] [-g] [-u] [-l] [-b] [-o] [-d] [-f]
                   [-p] [-w] [-m METHOD] [-n] [-v VERBOSITY]
                   [-S SINGLE] [-D DOUBLE] [-E --EQUAL] [-R --REPEAT ] [-B --BG ]
                   [-I --STDIN ] [ -P POS] [-H HEADER] [-A ANSWER] [-O fd]
                   [STDIN COMMAND [STDIN ARGS ...]]

positional arguments:
  STDIN COMMAND         Arguments to run the subprocess wih the password in stdin.

optional arguments:
  -h, --help            show this help message and exit
  -a TTYALERT, --ttyalert TTYALERT
                        Set the alert mode (none, beep or flash)
  -c                    Force concealment of entered text.
  -g, --grid            Ignore other options and behave like gpggrid.
  -u                    Allow uppercase letters [A-Z].
  -l                    Allow lowercase letters [a-z].
  -b                    Allow binary numbers [0-1].
  -o                    Allow octal numbers [0-7].
  -d                    Allow decimal numbers [0-9].
  -f                    Allow hexadecimal numbers [0-9][a-f][A-F].
  -p                    Allow punctuation.
  -w                    Allow whitespace.
  -m METHOD, --method METHOD
                        Specify indexing method (0-3, higher is more secure,
                        default=3)
  -n                    Append N newlines on output.
  -v VERBOSITY, --verbosity VERBOSITY
                        Specify verbosity (0-5, higher is verbose, default=3)
  -S SINGLE, --single SINGLE
                        Arg to pass to the subprocess with -SINGLE=password.
  -D DOUBLE, --double DOUBLE
                        Arg to pass to the subprocess with --DOUBLE=password.
  -E, --equal
                        Use = instead of space between single/double an arg.
  -R, --repeat
                        Repeat the password with a newline to the subprocess.
  -I, --stdin
                        Put the password on stdin to the subprocess.
  -B, --bg
                        Put the subprocess in the bg.
  -K descr, --session_add_key description
                        session_add_key 'some description'
  -P POS, --pos POS     Position to place the Arg to the subprocess with
                        --DOUBLE/SINGLE=password(default  0).
  -H HEADER, --header HEADER
                        Header line as a prompt (default "").
  -A ANSWER, --answer ANSWER
                        Skip the grid and give the answer (for testing only).

"""

#Learn more about Tinfoil Hat Linux:
#Homepage:  http://tinfoilhat.shmoo.com/
#Wikipedia: https://en.wikipedia.org/wiki/Tinfoil_Hat_Linux

#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

#TODO: Decide whether or not implement method 4
#TODO: Fix row index for non-default charsets

import os
import sys
import curses
import locale
import string
import random
import argparse
import _curses
from io import StringIO
import shlex
import shutil
import subprocess
from time import sleep
import traceback

sDir = os.path.dirname(os.path.realpath(__file__))
sDir in sys.path or sys.path.append(sDir)
try:
    from keepassxc_cmd2.cmd2_ansi import *
except Exception:
    sys.stderr.write('\n'.join(sys.path))
    def green(s): return s
    def red(s):   return s
    def yellow(s):   return s
    def blue(s):   return s


PY3 = sys.version_info[0] == 3
VERBOSITY = VERBOSE = 3
#
#    0: stdin
#    1: stdout
#    2: stderr
iOUT_FD = sys.stdout

try:
    import sh
except ImportError:
    sh = None

try:
    import keyutils
except ImportError:
    keyutils = None

try:
    from pyassuan.bin.pinentry import PinEntry
except ImportError: PinEntry = None

try:
    sPinentryCurses = shutil.which('pinentry-curses')
except Exception: sPinentryCurses = ''

try:
    from dialog import Dialog
except ImportError: Dialog = None

oDIALOG = None
oLAST_DIR = '/tmp/'

# Allow stderr for the caller to use
def debug(message):
    if VERBOSITY <= 4: return
    sys.stdout.write('DEBUG: ' + repr(message) +'\n')
    pass
def info(message):
    if VERBOSITY <= 3: return
    sys.stdout.write('INFO: ' + repr(message) +'\n')
    pass
def warn(message):
    if VERBOSITY <= 2: return
    sys.stdout.write('WARN: ' + repr(message) +'\n')
    pass
def error(message):
    if VERBOSITY <= 1: return
    sys.stdout.write('ERROR: ' + message +'\n')
    pass

class DummyContextManager:
    def __enter__(self):
        return self
    def __exit__(self, *exc):
        return False
try:
    devnull = subprocess.DEVNULL
except AttributeError: # Python < 3.3
    devnull_context = devnull = open(os.devnull, "wb")
else:
    devnull_context = DummyContextManager()


#Helps curses play nice
locale.setlocale(locale.LC_ALL, '')
code = locale.getpreferredencoding()

##### Defaults & Initial variables #####
charsets = ''
min_x, min_y = 55, 17
upperchar = ''
lowerchar = ''
out = ''
hide_out = 0
newline = 1

def spaceout(s):
    i=len(s)-1 #number of spaces to be inserted
    while i >= 1:
        s = s[:-i]+" "+s[-i:]
        i -= 1
    return s

def make_parser():
    """Argument parsing"""
    global VERBOSITY

    parser = argparse.ArgumentParser(add_help=True)
    parser.add_argument('-a', '--ttyalert', type=str,       help='Set the alert mode (none, beep or flash)')
    parser.add_argument('-c', help="Force concealment of entered text.", action="store_true")
    parser.add_argument('-g', '--grid', help="Ignore other options and behave like gpggrid.", action="store_true")
    #~ parser.add_argument('-u', dest='char_u', action='store_true')
    parser.add_argument('-u', help="Allow uppercase letters [A-Z].", action="store_true")
    parser.add_argument('-l', help="Allow lowercase letters [a-z].", action="store_true")
    parser.add_argument('-b', help="Allow binary numbers [0-1].", action="store_true")
    parser.add_argument('-o', help="Allow octal numbers [0-7].", action="store_true")
    parser.add_argument('-d', help="Allow decimal numbers [0-9].", action="store_true")
    parser.add_argument('-f', help="Allow hexadecimal numbers [0-9][a-f][A-F].", action="store_true")
    parser.add_argument('-p', help="Allow punctuation.", action="store_true")
    parser.add_argument('-w', help="Allow whitespace.", action="store_true")
    parser.add_argument('-m', '--method', help='Specify indexing method (0-3, higher is more secure, default=3)', dest='method', type=int, default=3)
    parser.add_argument('-n', help="Append newlines on output.", dest='newline', type=int, default=1)
    parser.add_argument('-v', '--verbosity', help='Specify verbosity (0-5, higher is verbose, default=' +str(VERBOSITY) +')', type=int, default=VERBOSITY)
    parser.add_argument('-S', '--single', help="Arg to pass to the subprocess with -SINGLE=password.", dest="single", default='')
    parser.add_argument('-D', '--double', help="Arg to pass to the subprocess with --DOUBLE=password.", dest="double", default='')
    parser.add_argument('-E', '--equal', help="Use = instead of space between single/double an arg.", action="store_true")
    parser.add_argument('-R', '--repeat', help="Repeat the password with a newline to the subprocess", action="store_true")
    parser.add_argument('-I', '--stdin', help="Send the password on stdin to the subprocess", action="store_true")
    if sh: parser.add_argument('-B', '--bg', help="Put the subprocess into the background", action="store_true")
    if keyutils:
        parser.add_argument('-K', '--session_add_key', help="session_add_key description", default='', dest='session_add_key', type=str)
    parser.add_argument('-P', '--pos', help="Position to place the Arg to the subprocess with --DOUBLE/SINGLE=password.", dest="pos", type=int, default=1)
    parser.add_argument('-H', '--header', help="Header line as a prompt (default  \"\").", dest="header", default='')
    parser.add_argument('-A', '--answer', help="Skip the grid and give the answer (for testing only).", dest="answer", default='')
    parser.add_argument('-O', '--output_fd', help="Output file descriptor to print the answer (default 1).", dest="output_fd", type=int, default=1)
    parser.add_argument('lsargs', help="Arguments to run the subprocess wih the password in stdin.", type=str, default='', nargs='*') # metavar='STDIN COMMAND',
    return parser

def make_grid(args):
    """Define character sets"""
    punct      = spaceout(string.punctuation)
    punct2     = punct[52:]
    punct      = punct[:52]
    whitespace = spaceout(string.whitespace)

    #Select character sets
    #TODO: Can this be simplified? args['u'+'l'] or whatnot?
    #use_uppercases = (-1 != charsets.find("u"))
    use_uppercases = args['u']
    use_lowercases = args['l']
    use_bindigits  = args['b']
    use_octdigits  = args['o']
    use_decdigits  = args['d']
    use_hexdigits  = args['f']
    use_punct      = args['p']
    use_whitespace = args['w']
    _setcount = args['u']+args['l']+args['b']+args['o']+args['d']+args['f']+args['p']+args['w']

    #if use_uppercases+use_lowercases+use_bindigits+use_octdigits+use_decdigits+use_hexdigits+use_punct+use_whitespace == 0:
    if _setcount == 0:
        use_uppercases = 1
        use_lowercases = 1
        use_decdigits  = 1
        use_punct      = 1
        use_whitespace = 1

    use_numbers    = 0
    numbers = ''
    if use_bindigits+use_octdigits+use_decdigits+use_hexdigits: use_numbers = 1
    if use_bindigits: numbers = '0 1'
    if use_octdigits: numbers = spaceout(string.octdigits)
    if use_decdigits: numbers = spaceout(string.digits)
    if use_hexdigits: numbers = spaceout(string.hexdigits)

    ##### Build static grid #####
    grid = []
    if use_uppercases:
        if PY3:
            grid.append(spaceout(string.ascii_uppercase))
        else:
            grid.append(spaceout(string.uppercase))
    if use_lowercases:
        if PY3:
            grid.append(spaceout(string.ascii_lowercase))
        else:
            grid.append(spaceout(string.lowercase))
    if use_numbers: grid.append(numbers)
    if use_punct:
        grid.append(punct)
        grid.append(punct2)
    return (use_uppercases,
                use_lowercases,
                use_decdigits,
                use_punct,
                use_whitespace,
                punct,
                punct2,
                whitespace,
                numbers,
                grid, )

##### Build user interface #####
usagebanner = "Choose from the grid by typing letter pairs like eF or Dg."

#? not hasattr(sys, 'frozen') and
if Dialog is not None and os.path.isfile('/usr/bin/dialog'):
    shortcuts = "5:Shell 6:FileName 7:FileContents 8:Hide 9:Quit 0:Done"
else:
    shortcuts = "7:Delete 8:Show/Hide 9:Quit 0:Done"

if sPinentryCurses:
    shortcuts = "4:PinEntry " +shortcuts

def total_rows(use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct):
    return use_lowercases+use_uppercases+use_decdigits+use_whitespace+use_punct+use_punct+1

def lMain(lSysArgv):
    global password, exit_code, VERBOSITY, iOUT_FD, oDIALOG, oLAST_DIR
    global min_x, min_y, upperchar, lowerchar, out, hide_out
    global oLAST_DIR

    if 'TERM' not in os.environ or not os.environ['TERM']: os.environ['TERM'] = linux

    padpos = 0

    stdscr = None
    password = ''
    exit_code = -1
    out = ''
    upperchar = ''
    lowerchar = ''

    if PY3:
        uppercase = string.ascii_uppercase
        lowercase = string.ascii_lowercase
    else:
        uppercase = string.uppercase
        lowercase = string.lowercase

    # Check for required size before continuing
    def check_size(stdscr):
        (size_y,size_x) = stdscr.getmaxyx()
        if ((size_x < min_x)|(size_y < min_y)):
            error( __program__+" needs a "+str(min_x)+"x"+str(min_y)+" character area to function.\n")
            error( "please adjust your terminal size and restart "+__program__+".")
            sys.exit(1)

    def pad_head(pad):
        global min_x, min_y, upperchar, lowerchar, out, hide_out
        pad.addstr(2,0,">>")
        pad.addstr(2,4," "*(min_x+1))
        disp_out = out
        if hide_out:
            disp_out = len(out)*"*"
        pad.addstr(2,4,disp_out)

    def pad_draw(stdscr, pad, ly, lx, hy, hx, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title):
        rv = __program__ +" "+ __version__
        pad.addstr(0,0," "*(hx+1),curses.A_REVERSE)
        pad.addstr(0,2,rv,curses.A_REVERSE)
        pad.addstr(1,0," "*min_x)
        #~ pad.addstr(0,40,"    ")
        pad.addstr(0,40,lowerchar,curses.A_REVERSE)
        pad.addstr(0,41,upperchar,curses.A_REVERSE)
        pad.addstr(0,73,"SHOW")
        if hide_out:
            pad.addstr(0,73,"HIDE")
        pad.addstr(1,0,header)
        pad_head(pad)

        ##Draw the grid
        global gridrow
        gridrow = 6
        gridoffset = 4

        #Grid Indexes
        pad.addstr(gridrow-2,gridoffset,spaceout(col_index))
        for t in range(0, total_rows(use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct)):
            pad.addstr(gridrow+t,1,row_index[t])
            t += 1

        #Index selection highlighting
        if len(upperchar):
            stdscr.addstr(gridrow-2,4+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        if len(lowerchar):
            stdscr.addstr(gridrow+row_index.find(lowerchar),1,lowerchar,curses.A_REVERSE)

        #Static grid elements
        ### New grid draw method ###
        r = 0
        for s in grid:
            pad.addstr(gridrow,gridoffset,grid[r])
            r += 1
            gridrow += 1

        if use_whitespace:
            #Draw the whitespace row
            pad.addstr(gridrow,gridoffset," :Space   :Tab     :Enter   :Clear")
            pad.addstr(gridrow,gridoffset,col_index[0])
            pad.addstr(gridrow,9+gridoffset,col_index[1])
            pad.addstr(gridrow,18+gridoffset,col_index[2])
            pad.addstr(gridrow,27+gridoffset,col_index[3])
            #If the corresponding columns are selected, highlight them
            if len(upperchar)&(upperchar==col_index[0]):
                stdscr.addstr(gridrow,4+col_index.find(upperchar),upperchar,curses.A_REVERSE)
            if len(upperchar)&(upperchar==col_index[1]):
                stdscr.addstr(gridrow,12+col_index.find(upperchar),upperchar,curses.A_REVERSE)
            if len(upperchar)&(upperchar==col_index[2]):
                stdscr.addstr(gridrow,20+col_index.find(upperchar),upperchar,curses.A_REVERSE)
            if len(upperchar)&(upperchar==col_index[3]):
                stdscr.addstr(gridrow,28+col_index.find(upperchar),upperchar,curses.A_REVERSE)

            gridrow += 1

        #Draw the 'done' line, and highlight column if selected
        pad.addstr(gridrow,gridoffset," :Ignore  :Double  :Triple  :Delete")
        pad.addstr(gridrow,gridoffset,col_index[0])
        pad.addstr(gridrow,9+gridoffset,col_index[1])
        pad.addstr(gridrow,18+gridoffset,col_index[2])
        pad.addstr(gridrow,27+gridoffset,col_index[3])
        if len(upperchar)&(upperchar==col_index[0]):
            stdscr.addstr(gridrow,4+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        if len(upperchar)&(upperchar==col_index[1]):
            stdscr.addstr(gridrow,11+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        if len(upperchar)&(upperchar==col_index[2]):
            stdscr.addstr(gridrow,18+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)
        if len(upperchar)&(upperchar==col_index[3]):
            stdscr.addstr(gridrow,25+spaceout(col_index).find(upperchar),upperchar,curses.A_REVERSE)


        #Help banners
        pad.addstr(14,0,title)
        pad.addstr(15,0,usagebanner)
        # (1+hx-len(shortcuts))*' '+
        pad.addstr(16,0,shortcuts,curses.A_REVERSE)

        pad.refresh(padpos, 0, ly, lx, hy, hx)

    def main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title):
        (max_y, max_x,) = stdscr.getmaxyx()
        pad_draw(stdscr, pad, 0, 0, (max_y-2), (max_x-1), use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title)
        stdscr.move(max_y-1,0)
        try:
            curses.curs_set(0)
        except:
            pass
        stdscr.refresh()

    def cycle_index(cyclemethod=3): #Argument specifies method, default is 3
        global col_index, row_index
        if PY3:
            uppercase = string.ascii_uppercase
            lowercase = string.ascii_lowercase
        else:
            uppercase = string.uppercase
            lowercase = string.lowercase
        #Method 0: Ordered letters and numbers, no randomization
        #This method has 1 possible state and provides no security.
        #Included to allow gridfire's use to limit possible symbols entered.
        #For example, hex digits only when entering a WEP key.
        if cyclemethod==0:
            col_index = uppercase
            row_index = lowercase

        #Method 1: Ordered letters, random offset (as with gpggrid)
        #The math might differ from gpggrid
        #This method has 676 or 26^2 possible states
        if cyclemethod==1:
            offset = random.randint(0,25)
            col_index = uppercase[offset:]+uppercase[:offset]
            offset = random.randint(0,25)
            row_index = lowercase[offset:]+lowercase[:offset]

        #Method 2: use random.shuffle() to shuffle one index in place
        #This might not work very well, see module documentation on
        #issues with small len(x) exceeding period of RNGs
        # http://docs.python.org/2/library/random.html
        ##This method has:
        ##403,291,461,126,605,635,584,000,000
        ##or 26! possible states (403.3 septillion)
        if cyclemethod==2:
            col_index = uppercase
            colcycle = list(uppercase)
            random.shuffle(colcycle)
            col_index = ''.join(colcycle)
            row_index = col_index.lower()

        #Method 3: use random.shuffle() to shuffle indices in place
        #This might not work very well, see module documentation on
        #issues with small len(x) exceeding period of RNGs
        # http://docs.python.org/2/library/random.html
        #This method has:
        #162,644,002,617,632,464,507,038,883,409,628,607,021,056,000,000,000,000
        ##or 26!^2 possible states (162.6 sexdecillion).
        if cyclemethod==3:
            col_index = uppercase
            colcycle = list(uppercase)
            random.shuffle(colcycle)
            col_index = ''.join(colcycle)

            row_index = lowercase
            rowcycle = list(lowercase)
            random.shuffle(rowcycle)
            row_index = ''.join(rowcycle)

        #TODO: Implement method 4 (mixed cases in row/column indexes)
            # (This would require redoing input handling to recognize same-case pairs)
        #Method 4 would have:
        #80,658,175,170,943,878,571,660,636,856,403,766,975,289,505,440,883,277,824,000,000,000,000
        #or 52! possible states (80.6 Unvigintillion).

    def quit_scr(leave_message = "", exit_status= 0):
        global password, exit_code
        stdscr.keypad(0)
        curses.echo()
        curses.nocbreak()
        curses.endwin()
        # print leave_message
        password = leave_message
        exit_code = exit_status

    parser = make_parser()
    args = vars(parser.parse_args(lSysArgv))

    VERBOSITY = args['verbosity']
    cyclemethod = args['method'] or 3
    header = args['header'] or ''
    answer = args['answer'] or ''
    output_fd = args['output_fd'] or 1
    lSargs = args['lsargs']

    if output_fd != 1:
        iOUT_FD = os.fdopen(int(output_fd), 'w')

    info('gridfire PARSE lSargs=' + repr(lSargs))
    debug('gridfire PARSE args.keys=' + repr(args.keys()))
    #? hangs if called as a sushell sys.stdout.flush()
    single = args['single'] or ''
    double = args['double'] or ''
    bRepeat = args['repeat']
    bStdin = args['stdin']
    bEqual = args['equal']

    if sh: bBg = args['bg']
    else: bBg = None
    if single and bRepeat:
            raise RuntimeError('ERROR: single and repeat ' )
    if double and bRepeat:
            raise RuntimeError('ERROR: double and repeat ' )
    if single and bRepeat:
            raise RuntimeError('ERROR: single and repeat ' )
    if double and bStdin:
            raise RuntimeError('ERROR: double and stdin ' )
    if double and single:
            raise RuntimeError('ERROR: double and single ' )

    (use_uppercases,
     use_lowercases,
     use_decdigits,
     use_punct,
     use_whitespace,
     punct,
     punct2,
     whitespace,
     numbers,
     grid ) = make_grid(args)

    if answer:
        # for testing - bypass the grid and accept the answer
        password = answer
        exit_code = 0
    else:
        if not stdscr:
            stdscr = curses.initscr()
            curses.noecho()
            curses.start_color()
            try:
                curses.use_default_colors()
            except:
                pass
            stdscr.keypad(1)

            for key, value in _curses.__dict__.items():
                if key[0:4] == 'ACS_' or key in ('LINES', 'COLS'):
                    if key == 'LINES':
                        value = value - 3
                    setattr(curses, key, value)

        curses.flushinp ()
        check_size(stdscr)
        pad = curses.newpad(100,100)
        if args['ttyalert'] == 'beep':
            curses.beep()
        elif args['ttyalert'] == 'flash':
            curses.flash()

        ##### MAIN LOOP: draw the interface and handle user input #####
        cycle_index(cyclemethod) #Don't start the program with unrandomized indexes
        # If this isn't done, the program initially shows nothing
        main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header,  '')

    while exit_code == -1:
        title = stderr_get_buffer().strip()
        if title:
            title = title.split('\n')[-1]
        main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header, title)
        curses.noecho()
        curses.cbreak()
        sys.stdout.flush()

        try:
            c = stdscr.getkey()
        except:
            quit_scr()
            break

        if c == '0':
            quit_scr(out) #on 'Done', write output and exit
            break
        if c in ['', '9', chr(27)]: # Quit
            quit_scr() #on 'Cancel', exit without writing output
            break
        if c == '8': # Hide
            hide_out -= hide_out+hide_out
            hide_out += 1
        elif ((c == '')&(len(out)>=1)) or ((c == 'KEY_BACKSPACE')&(len(out)>=1)):
            out = out[:len(out)-1] # Delete
        elif Dialog is not None and c == '7': # File Contents
            _curses.def_prog_mode() ; _curses.endwin()
            sOut = sDialogFget(bContents=True)
            if sOut == '':
               continue
            sBut = 'ok'; sCode = '='
            if sOut and out:
               sText = sOut # "New text at Beginning, Replace or End"
               sBut, sCode = lDialogCode(sText)
            if sBut != 'ok':
                pass
            elif sCode == '<':
                out = sOut + ' ' + out
            elif sCode == '>':
                out = out + ' ' + sOut
            elif sCode == '=':
                out = sOut
            elif sCode == '':
                out = ''
            pad_head(pad)

            # oDIALOG.msgbox("Bug Alert: If the screen goes black, suspend to shell with ^Z and then use fg to come back to Gridfire " +sCode, height=8, width=40)

            main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header,  '')
            _curses.reset_prog_mode()
            stdscr.refresh()

        elif Dialog is not None and c == '6': # File Name

# you will first need to save the tty modes with a call to def_prog_mode()
# and then call endwin() to end the curses mode. This will leave you in the
# original tty mode. To get back to curses once you are done, call
# reset_prog_mode() . This function returns the tty to the state stored by
# def_prog_mode(). Then do refresh(), and you are back to the curses mode.
            _curses.def_prog_mode() ; _curses.endwin()
            sOut = sDialogFget(bContents=False)
            sBut = 'ok'; sCode = '='
            if sOut and out:
               sText = sOut # "New text at Beginning, Replace or End"
               sBut, sCode = lDialogCode(sText)
            if sBut != 'ok':
                pass
            elif sCode == '<':
                out = sOut + ' ' + out
            elif sCode == '>':
                out = out + ' ' + sOut
            elif sCode == '=':
                out = sOut
            elif sCode == '':
                out = ''

            # oDIALOG.msgbox("Bug Alert: If the screen goes black, suspend to shell with ^Z and then use fg to come back to Gridfire " +sCode, height=8, width=40)

            main_draw(stdscr, pad, use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct, header,  '')
            _curses.reset_prog_mode()
            stdscr.refresh()

        elif Dialog is not None and c == '5': # Shell
            if oDIALOG is None:
                oDIALOG = Dialog()
            sCode = '='
            sOut = ''
            yStdoutdata = yStderrdata = b''

# you will first need to save the tty modes with a call to def_prog_mode()
# and then call endwin() to end the curses mode. This will leave you in the
# original tty mode. To get back to curses once you are done, call
# reset_prog_mode() . This function returns the tty to the state stored by
# def_prog_mode(). Then do refresh(), and you are back to the curses mode.
            _curses.def_prog_mode() ; _curses.endwin()
            sCode, sAnswer = oDIALOG.inputbox("Run command (erase to cancel)",
                                              init='')
            if sCode == 'ok' and sAnswer:

                with devnull_context:
                    p = subprocess.Popen(sAnswer,
                                         stdout=subprocess.PIPE,
                                         stderr=devnull, close_fds=True)
                    if True:
                        ( yStdoutdata, yStderrdata ) = p.communicate()
                        retcode = p.returncode
                        oDIALOG.scrollbox(yStdoutdata.decode().strip(),
                                          15,50)
                    else:
                        retcode = p.wait()
                        ystdoutdata = p.stdout.read()
                        # One could use title=... instead of text=... to put the text
                        # in the title bar.
                        oDIALOG.programbox(fd=p.stdout.fileno(),
                                        text="Output of command: " +sAnswer)

#                oDIALOG.msgbox("Bug Alert: If the screen goes black, suspend to shell with ^Z and then use fg to come back to Gridfire " +str(retcode),  height=8, width=40)

                if retcode == 0:
                    sOut = yStdoutdata.decode().strip()
                sBut = 'ok'; sCode = '='
                if sOut and out:
                   sText = sOut # "New text at Beginning, Replace or End"
                   sBut, sCode = lDialogCode(sText)
                if sBut != 'ok':
                    pass
                elif sCode == '<':
                    out = sOut + ' ' + out
                elif sCode == '>':
                    out = out + ' ' + sOut
                elif sCode == '=':
                    out = sOut
                elif sCode == '':
                    out = ''
                _curses.reset_prog_mode()
                stdscr.refresh()

        elif c == '4' and sPinentryCurses:
            _curses.def_prog_mode() ; _curses.endwin()
            iCode, sOut =  pinentry_assuan_getpass()
            if iCode == 0:
                out += sOut
                pad_head(pad)
            _curses.reset_prog_mode()
            stdscr.refresh()

        elif c == '2': # was Ignore
            # now 'Ignore' is a keypair to fool keystroke counting
            pass # out = out*2
        elif PinEntry is not None and c == '1': # unused
            if oPinEntry is None:
                oPinEntry = PinEntry()
            oPinEntry.run()
            # unfinished
        #handle row/column selections
        elif (uppercase.find(c)+1):
            upperchar = c
        elif (row_index[:total_rows(use_lowercases, use_uppercases, use_decdigits, use_whitespace, use_punct)].find(c)+1):
            lowerchar = c

        #if selection completes a pair, find choice, add to output, reset selection
        if len(upperchar)&len(lowerchar):
            # Ignore Double Triple
            if((upperchar==col_index[0])&(lowerchar==row_index[len(grid)+1])):
                #on 'Ignore', do nothing
                pass
            elif((upperchar==col_index[1])&(lowerchar==row_index[len(grid)+1])):
                # Double
                out += out
            elif((upperchar==col_index[2])&(lowerchar==row_index[len(grid)+1])):
                # Triple
                out = 3*out
            elif((upperchar==col_index[3])&(lowerchar==row_index[len(grid)+1])):
                out = out[:len(out)-1] # Delete

            #  :Space  :Tab  :Enter :Clear
            elif((upperchar==col_index[0])&(lowerchar==row_index[len(grid)])):
                out += ' ' #on 'Space'
            elif((upperchar==col_index[1])&(lowerchar==row_index[len(grid)])):
                out += '	' #on 'Tab'
            elif((upperchar==col_index[2])&(lowerchar==row_index[len(grid)])):
                out += '\n' #on 'Enter'
            elif((upperchar==col_index[3])&(lowerchar==row_index[len(grid)])):
                out = '' #on 'Clear'
                pad_head(pad)
            else:
                rowchoice = row_index.find(lowerchar)
                if rowchoice < len(grid):
                    if (grid[rowchoice].find("A") == 0):
                        out += uppercase[col_index.find(upperchar)]
                    if (grid[rowchoice].find("a") == 0):
                        out += lowercase[col_index.find(upperchar)]
                    if (grid[rowchoice].find("0") == 0)&(spaceout(col_index).find(upperchar) < len(numbers)):
                        out += numbers[spaceout(col_index).find(upperchar)]
                    if (grid[rowchoice].find("!") == 0):
                        out += punct[spaceout(col_index).find(upperchar)]
                    if (grid[rowchoice].find("_") == 0)&(spaceout(col_index).find(upperchar) < len(punct2)):
                        out += punct2[spaceout(col_index).find(upperchar)]

            upperchar = ''
            lowerchar = ''
            cycle_index(cyclemethod)

    debug('END MAINLOOP lSargs=' +repr(lSargs) +' len=' +str(len(password)) \
                                 +' exit_code=' +str(exit_code))
    sRetval = ''
    if exit_code == 0 and lSargs and password:
        try:
            if os.uname().sysname == 'Linux':
                sDash = '-'
            else:
                # veracrypt win requires /
                sDash = '/'
            sStdin = ''
            lThings = []
            if single:
                # trouble- password=shlex.quote(password)
                if bEqual:
                    lThings = [sDash+single +'=' +password]
                else:
                    lThings = [sDash+single, password]
            elif double:
                # truecrypt requires =
                if bEqual:
                    lThings = ['--'+double +'=' +password]
                else:
                    lThings = ['--'+double, password]
            elif bRepeat:
                sStdin = password +'\n' +password
            else:
                sStdin = password

            if lThings:
                pos = args['pos']
                if pos == 0:
                    largs = lThings + lSargs
                elif pos == -1:
                    largs = lSargs + lThings
                else:
                    largs = lSargs[0:pos] +lThings + lSargs[pos:]
            else:
                largs = lSargs

            try:
                if largs.index('--') >= 0:
                    del largs[largs.index('--')]
            except ValueError: pass

            # if the password contains a space assume the first elt is
            # the password and the rest are command line addons
            if password.find(' ') > 0:
                lCargs = shlex.split(password)
                largs += lCargs[1:]
                password = lCargs[0]

            if keyutils:
                if not args['session_add_key']:
                    if len(largs) > 0 and os.path.isfile(largs[-1]):
                        args['session_add_key'] = os.path.split(largs[-1])[-1]
                if args['session_add_key']:
                    yVal = password.encode()
                    yDesc=args['session_add_key'].encode()
                    session_add_key(yDesc, yVal)

            env = os.environ.copy()
            if 'PYTHONPATH' in env:
                del env['PYTHONPATH']
            executable = shutil.which(largs[0])
            if not executable:
                raise RuntimeError('ERROR: executable not found ' +str(repr(largs[0])))

            _curses.def_prog_mode() ; _curses.endwin()
            info('executable=' +executable +' lArgs[1:]=%r' % (largs[1:]),)
            # debug(' len(password)=%d' %  len(password))

            stderr_to_buffer()
            global stdoutdata, stderrdata, iRetval
            iRetval = 0; stdoutdata = stderrdata = ''
            if bBg and sh is None:
                warn("Ignoring  -B--bg - no import sh")
                bBg = False
            elif bBg:
                debug('sh sStdin bBg=True len(sStdin)=%d' %  len(sStdin))
                try:
                    # oProc=getattr(sh, executable)
                    oProc = __import__('sh', fromlist=[executable])
                    def process_output(line):
                        global stdoutdata, exit_code
                        stdoutdata   += line
                        if exit_code != -1: return True
                    def process_errput(line):
                        sys.stderr.write('STDERR: ' +line.decode() + '\n')
                        global stderrdata, exit_code
                        stderrdata   += line
                        if exit_code != -1: return True
                    oProcess=oProc(*largs[1:],
                                   _in=StringIO(sStdin),
                                   _out=process_output,
                                   _err=process_errput,
                                   _bg=True)
                except Exception as e:
                    error('ERROR in sh.oProc ' +str(e))
                    iRetval = 1
                    password = ''
            else:
                shell = False # bBg or
                # if bBg: largs += ['&']
                debug('Popen bBg='+repr(bBg) +' len(sStdin)=%d' % len(sStdin))
                try:
                    process = subprocess.Popen(largs,
                                           stdin=subprocess.PIPE,
                                           stdout=subprocess.PIPE,
                                           stderr=subprocess.PIPE,
                                           env=env,
                                           shell=shell)
                    # close_fds=True
                except Exception as e:
                    sMsg='ERROR in Popen ' +str(e)
                    error(sMsg)
                    lDialogMessage(sMsg,height=20,width=60)
                    iRetval = 1
                    password = ''
                else:
                    if sStdin:
                        (stdoutdata, stderrdata,) = process.communicate(sStdin.encode())
                    else:
                        (stdoutdata, stderrdata,) = process.communicate(None)
                    iRetval = process.returncode
                    for pipe in (process.stdin, process.stdout, process.stderr):
                        if pipe: pipe.close()

            stderrdata=stderrdata.strip()
            if stderrdata:
                if hasattr (stderrdata, 'decode'):
                    stderrdata=stderrdata.decode()

                sys.stderr.write('STDERR: ' +stderrdata + '\n')
            sMess='stdoutdata=' +repr(stdoutdata) \
                    +' stderrdata=' +repr(stderrdata) \
                    +' returncode=' +str(iRetval)
#            if stderrdata.find('Failed') >= 0:
#                lDialogMessage(sMess,height=20,width=60)
            if iRetval:
                warn(sMess)
                lDialogMessage(sMess,height=20,width=60)
            else:
                info(sMess)

            if hasattr (stdoutdata, 'decode'):
                sRetval = stdoutdata.decode().strip()
            else:
                sRetval = stdoutdata.strip()
            debug('len(sRetval)=%d' %  len(sRetval) +' iRetval=' +str(iRetval))
            exit_code = iRetval
        except Exception as e:
            sMsg = 'ERROR after mainloop error=' +str(e) +' len=' +str(len(password))
            error(sMsg)
            lDialogMessage(sMsg)
            warn(traceback.format_exc())
            exit_code = 1
        stderr_un_buffer()

    else:
        debug('NO exit_code=' +str(exit_code) +' lSargs=' +repr(lSargs) +' len(password)=' +str(len(password)))
        sRetval = password

    # del sRetval, exit_code
    stdscr = None
    cycle_index(cyclemethod) #Don't start the program with unrandomized indexes
    lRetval = [exit_code, sRetval][:]
    out = ''
    exit_code = -1

    return lRetval

def get_tty(stream=None, tty=None):
    # what should stream be if curses should be a tty
    if tty is not None:
        pass
    elif 'GPG_TTY' in os.environ:
        tty = os.environ['GPG_TTY']
    elif stream is not None:
        tty = os.ttyname(stream.fileno())
    else:
        tty = os.ttyname(sys.stdin.fileno())

    with open(tty, 'r') as fd:
        assert fd.isatty(), "The input stream must be a tty: %r" % (tty,)
    return tty

oSTDERR=StringIO()
def stderr_to_buffer():
    global oSTDERR
    if not hasattr(sys, '_stderr'):
        sys._stderr = sys.stderr
    sys.stderr = oSTDERR

def stderr_un_buffer():
    if hasattr(sys, '_stderr'):
        sys.stderr = sys._stderr

def stderr_get_buffer():
    sys.stderr.flush()
    if hasattr(sys.stderr, 'seek') and sys.stderr.seekable():
        sys.stderr.seek(0)
    if hasattr(sys.stderr, 'getvalue'):
        title = sys.stderr.getvalue().strip()
        if hasattr(sys.stderr, 'seek') and sys.stderr.seekable():
            sys.stderr.seek(0)
        elif hasattr(sys.stderr, 'buf'):
            sys.stderr.buf = ''
    else:
        title = ''
    return title

# should be getpass.getpass compatible
def getpass(prompt="", stream=None, tty=None, main_args=None):
    tty = get_tty(stream, tty)
    #?
    if not hasattr(sys, '_stderr'):
        sys._stderr = sys.stderr
        sys.stderr = StringIO()

    sRetval = ''
    exit_code = -1
    if main_args is None:
        main_args = ['-H', prompt]
    else:
        main_args += ['-H', prompt]
    try:
        (exit_code, sRetval,) = main(main_args)
        info('EXIT: getpass ' + prompt +' ' + repr(sRetval))
    except KeyboardInterrupt:
        info('EXIT: getpass KeyboardInterrupt')

    #?
    if hasattr(sys, '_stderr'):
        sys.stderr = sys._stderr

    return sRetval


def session_add_key(yDesc,yVal):

    from keyutils import join_session_keyring, add_key, request_key, set_perm

    iSk=join_session_keyring()
    assert type(iSk) == int

    iKey = add_key(yDesc, yVal, iSk)
    assert iKey >= 0

    key_id=request_key(yDesc, iSk)
    assert key_id > 0

    set_perm(key_id, 0x3f1f0000)
    info('session_add_key(' +yDesc.decode() +')')

def sDialogFget(bContents=True):
    global oLAST_DIR
    sBut, sCode = lDialogFselect(oLAST_DIR, height=10, width=50, help_button=False)
    sOut = ''
    if sBut == 'ok':
        sFile = sCode
        if bContents is not True:
            return sFile
        try:
            with open( sFile, 'rt') as iFd:
                sOut = iFd.read().strip()
        except Exception as e:
            # dunno
            pass
        else:
            if os.path.isdir(sFile):
                oLAST_DIR = sFile
            else:
                oLAST_DIR = os.path.dirname(sFile)
            oLAST_DIR = oLAST_DIR + '/'
    return sOut

def lDialogMessage(sMess, height=10, width=50, help_button=False, prompt=None):
    global oDIALOG, oLAST_DIR
    if oDIALOG is None:
        oDIALOG = Dialog()
    sBut, sCode = oDIALOG.msgbox(sMess, height, width, help_button=False)
    return (sBut, sCode,)

def lDialogFselect(sLastDir=None, height=10, width=50, help_button=False, prompt=None):
    global oDIALOG, oLAST_DIR
    if oDIALOG is None: oDIALOG = Dialog()
    if not sLastDir: sLastDir = oLAST_DIR
    sBut, sCode = oDIALOG.fselect(sLastDir, height, width, help_button=False)
    return (sBut, sCode,)

def lDialogCode(sText):
   lChoices = [("<", "Begin - Add the text at the beginning"),
               ("=", "All - Replace all with the text"),
               (">", "End - Add the text at the end")]
   sCode = '='
   sBut, sCode = oDIALOG.menu(sText, height=16, width=50,
                     choices=lChoices,
                     title="Replace the buffer with file contents",
                     help_button=False)
   return (sBut, sCode,)

def pinentry_assuan_getpass():
    # simply prints out commands for pinentry's stdin to activate the
    # password dialog
    sStdIn='''OPTION lc-ctype=C
SETTITLE Enter the text that you want added to gridfire
SETDESC  Enter the text that you want added to gridfire
SETPROMPT Text:
GETPIN
'''
    sStderr = sRetval = ''
    largs = [sPinentryCurses]
# -T, --ttyname FILE         Set the tty terminal node name
# -N, --ttytype NAME         Set the tty terminal type
# -C, --lc-ctype STRING      Set the tty LC_CTYPE value
# -M, --lc-messages STRING   Set the tty LC_MESSAGES value
# -o, --timeout SECS         Timeout waiting for input after this many seconds
# -g, --no-global-grab       Grab keyboard only while window is
    tty = get_tty()
    largs += ['-T', tty, '-g']
    #S ERROR curses.open_tty_for_read 83918849 \\nERR 83918849 Permission denied <Pinentry>
    if os.getegid() == 0 and 'SUDO_USER' in os.environ:
        o=os.stat(tty)
        if o.st_uid != 0:
            if hasattr(shlex, 'join'):
                largs =['su', '-c', shlex.join(largs), os.environ['SUDO_USER']]
            else:
                largs =['su', '-c', ' '.join(largs), os.environ['SUDO_USER']]

    stderr_to_buffer()
    try:
        sStdIn='OPTION ttyname=' +tty +'\n' +sStdIn
        debug('pinentry_assuan_getpass tty=' +repr(tty))
        (exit_code, sStdOut,) = lSubprocess(largs, sStdIn=sStdIn)
        if exit_code == 0:
            for sLine in sStdOut.split('\n'):
                if sLine[:2] == 'D ':
                    sRetval = sLine[2:]
                    break
                elif sLine.startswith('ERR') or sLine.startswith('S ERR'):
                    exit_code = 1
                    sys.stderr.write(sLine +'\n')
                    warn('pinentry_assuan_getpass ' +sLine) # +'\n'
        if exit_code:
            warn('pinentry_assuan_getpass \nexit_code=' + str(exit_code))
        else:
            debug('pinentry_assuan_getpass len sRetval=' +str(len(sRetval)))
    except Exception as e:
        error('pinentry_assuan_getpass exception=' + str(e))
        exit_code = 1
    stderr_un_buffer()

    return (exit_code, sRetval.strip(),)

def lSubprocess(largs, shell=False, executable='', sStdIn=''):
    stderr_to_buffer()

    env = os.environ.copy()
    if 'PYTHONPATH' in env:
        del env['PYTHONPATH']

    yStdoutData = stderrdata = ''
    try:
        debug('lSubprocess Popen largs=' + repr(largs) +' len(Stdin)=' +str(len(sStdIn)))
        oProcess = subprocess.Popen(largs,
                               stdin=subprocess.PIPE,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE,
                               env=env,
                               shell=shell)
        sleep(3)
    except Exception as e:
        error('Popen executable=' + repr(executable) +' largs=' + repr(largs) + '\n' + str(e))
        exit_code = 1
        sStdOut = ''
    else:
        if sStdIn:
            (yStdoutData, stderrdata,) = oProcess.communicate(sStdIn.encode())
        else:
            #? wait
            (yStdoutData, stderrdata,) = oProcess.communicate()
        #? oProcess.close()
        if stderrdata:
            sys.stderr.write(stderrdata.decode() + '\n')
        exit_code = oProcess.returncode
        sStdOut = yStdoutData.decode()
    info('Popen len sStdOut=' +str(len(sStdOut)) +' exit_code=' + repr(exit_code) )
    stderr_un_buffer()
    return (exit_code, sStdOut,)

def iMain(lSysArgv=None):
    if not lSysArgv: lSysArgv=sys.argv[1:]

    try:
        stderr_to_buffer()
        (exit_code, password,) = lMain(lSysArgv)
        stderr_un_buffer()

    except Exception as e:
        lDialogMessage('ERROR: IMAIN ' + str(e))
        exit_code = 1
        password = ''
        warn('ERROR: Leaving lIMain exit_code=1 ' + str(e))
        debug(traceback.format_exc())
    else:
        debug('leaving lIMain exit_code=' + repr(exit_code))

    sys.stderr.flush()
    iOUT_FD.write(password+('\n'*newline))
    iOUT_FD.flush()
    password = ''

    return exit_code

def main():
   sys.exit(iMain())

if __name__ == '__main__':
   main()

if False:
    lSargs = []
    if lSargs:
        if type(lSargs) == str:
            #? quote - unused - it's always a list
            largs = lSargs.split(' ')
        elif type(lSargs) == list and len(lSargs) == 1 and lSargs[0].find(' ') > 0:
            largs = lSargs[0].strip().split()
        elif type(lSargs) == list and len(lSargs) > 1:
            largs = lSargs
        else:
            raise RuntimeError('ERROR: not list or str type(lSargs) ' + str(type(lSargs) ))

