#! /usr/bin/env python
#
# $Id: teampasswd 464 2016-07-14 11:27:39Z sanders $
#
# This scripts queries TeamPasswordManagers's API and shows lists of
# passwords and/or password details.
#
# See https://wiki.bit.nl/wiki/index.php/WachtwoordBeheer for details
# and http://teampasswordmanager.com/docs/api/ for API information


import urllib, urllib2, simplejson, getpass, base64, sys, argparse
import ConfigParser, copy, string, os, time, hashlib
from subprocess import Popen, PIPE


# location of the API
BASE_URL    = "https://passwords.bit.nl/index.php"
API_URL     = "%s/api/v2" % BASE_URL

# config files
BACE_CONFIG  = "/etc/bace/bace.ini"
LOCAL_CONFIG = os.path.expanduser("~/.teampasswd")

# Rijndael blocksize
BLOCK_SIZE  = 32

# Debugging
DEBUG       = False

# expiry status (taken from API docs)
EXP_STATUS = {
    0 : "Active",
    1 : "Expires today",
    2 : "Expired",
    3 : "Expires soon",
}


# ------ Rijndael class for token decryption --------
# Taken from https://gist.github.com/jeetsukumaran/1291836
# --------------------------------------------------------

class Rijndael(object):
    """
    A pure python (slow) implementation of rijndael with a decent interface.

    To do a key setup::

        r = Rijndael(key, block_size = 16)

    key must be a string of length 16, 24, or 32
    blocksize must be 16, 24, or 32. Default is 16

    To use::

        ciphertext = r.encrypt(plaintext)
        plaintext = r.decrypt(ciphertext)

    If any strings are of the wrong length a ValueError is thrown
    """


    @classmethod
    def create(cls):

        if hasattr(cls, "RIJNDAEL_CREATED"):
            return

        # [keysize][block_size]
        cls.num_rounds = {16: {16: 10, 24: 12, 32: 14}, 24: {16: 12, 24: 12, 32: 14}, 32: {16: 14, 24: 14, 32: 14}}

        cls.shifts = [[[0, 0], [1, 3], [2, 2], [3, 1]],
                [[0, 0], [1, 5], [2, 4], [3, 3]],
                [[0, 0], [1, 7], [3, 5], [4, 4]]]

        A = [[1, 1, 1, 1, 1, 0, 0, 0],
            [0, 1, 1, 1, 1, 1, 0, 0],
            [0, 0, 1, 1, 1, 1, 1, 0],
            [0, 0, 0, 1, 1, 1, 1, 1],
            [1, 0, 0, 0, 1, 1, 1, 1],
            [1, 1, 0, 0, 0, 1, 1, 1],
            [1, 1, 1, 0, 0, 0, 1, 1],
            [1, 1, 1, 1, 0, 0, 0, 1]]

        # produce log and alog tables, needed for multiplying in the
        # field GF(2^m) (generator = 3)
        alog = [1]
        for i in xrange(255):
            j = (alog[-1] << 1) ^ alog[-1]
            if j & 0x100 != 0:
                j ^= 0x11B
            alog.append(j)

        log = [0] * 256
        for i in xrange(1, 255):
            log[alog[i]] = i

        # multiply two elements of GF(2^m)
        def mul(a, b):
            if a == 0 or b == 0:
                return 0
            return alog[(log[a & 0xFF] + log[b & 0xFF]) % 255]

        # substitution box based on F^{-1}(x)
        box = [[0] * 8 for i in xrange(256)]
        box[1][7] = 1
        for i in xrange(2, 256):
            j = alog[255 - log[i]]
            for t in xrange(8):
                box[i][t] = (j >> (7 - t)) & 0x01

        B = [0, 1, 1, 0, 0, 0, 1, 1]

        # affine transform:  box[i] <- B + A*box[i]
        cox = [[0] * 8 for i in xrange(256)]
        for i in xrange(256):
            for t in xrange(8):
                cox[i][t] = B[t]
                for j in xrange(8):
                    cox[i][t] ^= A[t][j] * box[i][j]

        # cls.S-boxes and inverse cls.S-boxes
        cls.S =  [0] * 256
        cls.Si = [0] * 256
        for i in xrange(256):
            cls.S[i] = cox[i][0] << 7
            for t in xrange(1, 8):
                cls.S[i] ^= cox[i][t] << (7-t)
            cls.Si[cls.S[i] & 0xFF] = i

        # T-boxes
        G = [[2, 1, 1, 3],
            [3, 2, 1, 1],
            [1, 3, 2, 1],
            [1, 1, 3, 2]]

        AA = [[0] * 8 for i in xrange(4)]

        for i in xrange(4):
            for j in xrange(4):
                AA[i][j] = G[i][j]
                AA[i][i+4] = 1

        for i in xrange(4):
            pivot = AA[i][i]
            if pivot == 0:
                t = i + 1
                while AA[t][i] == 0 and t < 4:
                    t += 1
                    assert t != 4, 'G matrix must be invertible'
                    for j in xrange(8):
                        AA[i][j], AA[t][j] = AA[t][j], AA[i][j]
                    pivot = AA[i][i]
            for j in xrange(8):
                if AA[i][j] != 0:
                    AA[i][j] = alog[(255 + log[AA[i][j] & 0xFF] - log[pivot & 0xFF]) % 255]
            for t in xrange(4):
                if i != t:
                    for j in xrange(i+1, 8):
                        AA[t][j] ^= mul(AA[i][j], AA[t][i])
                    AA[t][i] = 0

        iG = [[0] * 4 for i in xrange(4)]

        for i in xrange(4):
            for j in xrange(4):
                iG[i][j] = AA[i][j + 4]


        def mul4(a, bs):
            if a == 0:
                return 0
            r = 0
            for b in bs:
                r <<= 8
                if b != 0:
                    r = r | mul(a, b)
            return r

        cls.T1 = []
        cls.T2 = []
        cls.T3 = []
        cls.T4 = []
        cls.T5 = []
        cls.T6 = []
        cls.T7 = []
        cls.T8 = []
        cls.U1 = []
        cls.U2 = []
        cls.U3 = []
        cls.U4 = []

        for t in xrange(256):
            s = cls.S[t]
            cls.T1.append(mul4(s, G[0]))
            cls.T2.append(mul4(s, G[1]))
            cls.T3.append(mul4(s, G[2]))
            cls.T4.append(mul4(s, G[3]))

            s = cls.Si[t]
            cls.T5.append(mul4(s, iG[0]))
            cls.T6.append(mul4(s, iG[1]))
            cls.T7.append(mul4(s, iG[2]))
            cls.T8.append(mul4(s, iG[3]))

            cls.U1.append(mul4(t, iG[0]))
            cls.U2.append(mul4(t, iG[1]))
            cls.U3.append(mul4(t, iG[2]))
            cls.U4.append(mul4(t, iG[3]))

        # round constants
        cls.rcon = [1]
        r = 1
        for t in xrange(1, 30):
            r = mul(2, r)
            cls.rcon.append(r)

        cls.RIJNDAEL_CREATED = True


    def __init__(self, key, block_size = 16):

        # create common meta-instance infrastructure
        self.create()

        if block_size != 16 and block_size != 24 and block_size != 32:
            raise ValueError('Invalid block size: ' + str(block_size))
        if len(key) != 16 and len(key) != 24 and len(key) != 32:
            raise ValueError('Invalid key size: ' + str(len(key)))
        self.block_size = block_size

        ROUNDS = Rijndael.num_rounds[len(key)][block_size]
        BC = block_size / 4
        # encryption round keys
        Ke = [[0] * BC for i in xrange(ROUNDS + 1)]
        # decryption round keys
        Kd = [[0] * BC for i in xrange(ROUNDS + 1)]
        ROUND_KEY_COUNT = (ROUNDS + 1) * BC
        KC = len(key) / 4

        # copy user material bytes into temporary ints
        tk = []
        for i in xrange(0, KC):
            tk.append((ord(key[i * 4]) << 24) | (ord(key[i * 4 + 1]) << 16) |
                (ord(key[i * 4 + 2]) << 8) | ord(key[i * 4 + 3]))

        # copy values into round key arrays
        t = 0
        j = 0
        while j < KC and t < ROUND_KEY_COUNT:
            Ke[t / BC][t % BC] = tk[j]
            Kd[ROUNDS - (t / BC)][t % BC] = tk[j]
            j += 1
            t += 1
        tt = 0
        rconpointer = 0
        while t < ROUND_KEY_COUNT:
            # extrapolate using phi (the round key evolution function)
            tt = tk[KC - 1]
            tk[0] ^= (Rijndael.S[(tt >> 16) & 0xFF] & 0xFF) << 24 ^  \
                     (Rijndael.S[(tt >>  8) & 0xFF] & 0xFF) << 16 ^  \
                     (Rijndael.S[ tt        & 0xFF] & 0xFF) <<  8 ^  \
                     (Rijndael.S[(tt >> 24) & 0xFF] & 0xFF)       ^  \
                     (Rijndael.rcon[rconpointer]    & 0xFF) << 24
            rconpointer += 1
            if KC != 8:
                for i in xrange(1, KC):
                    tk[i] ^= tk[i-1]
            else:
                for i in xrange(1, KC / 2):
                    tk[i] ^= tk[i-1]
                tt = tk[KC / 2 - 1]
                tk[KC / 2] ^= (Rijndael.S[ tt        & 0xFF] & 0xFF)       ^ \
                              (Rijndael.S[(tt >>  8) & 0xFF] & 0xFF) <<  8 ^ \
                              (Rijndael.S[(tt >> 16) & 0xFF] & 0xFF) << 16 ^ \
                              (Rijndael.S[(tt >> 24) & 0xFF] & 0xFF) << 24
                for i in xrange(KC / 2 + 1, KC):
                    tk[i] ^= tk[i-1]
            # copy values into round key arrays
            j = 0
            while j < KC and t < ROUND_KEY_COUNT:
                Ke[t / BC][t % BC] = tk[j]
                Kd[ROUNDS - (t / BC)][t % BC] = tk[j]
                j += 1
                t += 1
        # inverse MixColumn where needed
        for r in xrange(1, ROUNDS):
            for j in xrange(BC):
                tt = Kd[r][j]
                Kd[r][j] = Rijndael.U1[(tt >> 24) & 0xFF] ^ \
                           Rijndael.U2[(tt >> 16) & 0xFF] ^ \
                           Rijndael.U3[(tt >>  8) & 0xFF] ^ \
                           Rijndael.U4[ tt        & 0xFF]
        self.Ke = Ke
        self.Kd = Kd


    def encrypt(self, plaintext):
        if len(plaintext) != self.block_size:
            raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(plaintext)))
        Ke = self.Ke

        BC = self.block_size / 4
        ROUNDS = len(Ke) - 1
        if BC == 4:
            Rijndael.SC = 0
        elif BC == 6:
            Rijndael.SC = 1
        else:
            Rijndael.SC = 2
        s1 = Rijndael.shifts[Rijndael.SC][1][0]
        s2 = Rijndael.shifts[Rijndael.SC][2][0]
        s3 = Rijndael.shifts[Rijndael.SC][3][0]
        a = [0] * BC
        # temporary work array
        t = []
        # plaintext to ints + key
        for i in xrange(BC):
            t.append((ord(plaintext[i * 4    ]) << 24 |
                      ord(plaintext[i * 4 + 1]) << 16 |
                      ord(plaintext[i * 4 + 2]) <<  8 |
                      ord(plaintext[i * 4 + 3])        ) ^ Ke[0][i])
        # apply round transforms
        for r in xrange(1, ROUNDS):
            for i in xrange(BC):
                a[i] = (Rijndael.T1[(t[ i           ] >> 24) & 0xFF] ^
                        Rijndael.T2[(t[(i + s1) % BC] >> 16) & 0xFF] ^
                        Rijndael.T3[(t[(i + s2) % BC] >>  8) & 0xFF] ^
                        Rijndael.T4[ t[(i + s3) % BC]        & 0xFF]  ) ^ Ke[r][i]
            t = copy.copy(a)
        # last round is special
        result = []
        for i in xrange(BC):
            tt = Ke[ROUNDS][i]
            result.append((Rijndael.S[(t[ i           ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
            result.append((Rijndael.S[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
            result.append((Rijndael.S[(t[(i + s2) % BC] >>  8) & 0xFF] ^ (tt >>  8)) & 0xFF)
            result.append((Rijndael.S[ t[(i + s3) % BC]        & 0xFF] ^  tt       ) & 0xFF)
        return string.join(map(chr, result), '')


    def decrypt(self, ciphertext):
        if len(ciphertext) != self.block_size:
            raise ValueError('wrong block length, expected ' + str(self.block_size) + ' got ' + str(len(ciphertext)))
        Kd = self.Kd

        BC = self.block_size / 4
        ROUNDS = len(Kd) - 1
        if BC == 4:
            Rijndael.SC = 0
        elif BC == 6:
            Rijndael.SC = 1
        else:
            Rijndael.SC = 2
        s1 = Rijndael.shifts[Rijndael.SC][1][1]
        s2 = Rijndael.shifts[Rijndael.SC][2][1]
        s3 = Rijndael.shifts[Rijndael.SC][3][1]
        a = [0] * BC
        # temporary work array
        t = [0] * BC
        # ciphertext to ints + key
        for i in xrange(BC):
            t[i] = (ord(ciphertext[i * 4    ]) << 24 |
                    ord(ciphertext[i * 4 + 1]) << 16 |
                    ord(ciphertext[i * 4 + 2]) <<  8 |
                    ord(ciphertext[i * 4 + 3])        ) ^ Kd[0][i]
        # apply round transforms
        for r in xrange(1, ROUNDS):
            for i in xrange(BC):
                a[i] = (Rijndael.T5[(t[ i           ] >> 24) & 0xFF] ^
                        Rijndael.T6[(t[(i + s1) % BC] >> 16) & 0xFF] ^
                        Rijndael.T7[(t[(i + s2) % BC] >>  8) & 0xFF] ^
                        Rijndael.T8[ t[(i + s3) % BC]        & 0xFF]  ) ^ Kd[r][i]
            t = copy.copy(a)
        # last round is special
        result = []
        for i in xrange(BC):
            tt = Kd[ROUNDS][i]
            result.append((Rijndael.Si[(t[ i           ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF)
            result.append((Rijndael.Si[(t[(i + s1) % BC] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF)
            result.append((Rijndael.Si[(t[(i + s2) % BC] >>  8) & 0xFF] ^ (tt >>  8)) & 0xFF)
            result.append((Rijndael.Si[ t[(i + s3) % BC]        & 0xFF] ^  tt       ) & 0xFF)
        return string.join(map(chr, result), '')

    # @staticmethod
    # def encrypt_block(key, block):
    #     return Rijndael(key, len(block)).encrypt(block)

    # @staticmethod
    # def decrypt_block(key, block):
    #     return Rijndael(key, len(block)).decrypt(block)

    @staticmethod
    def test():
        def t(kl, bl):
            b = 'b' * bl
            r = Rijndael('a' * kl, bl)
            x = r.encrypt(b)
            assert x != b
            assert r.decrypt(x) == b
        t(16, 16)
        t(16, 24)
        t(16, 32)
        t(24, 16)
        t(24, 24)
        t(24, 32)
        t(32, 16)
        t(32, 24)
        t(32, 32)

# -------------------------- end of Rijndael ----------------------------


def invisible(text):
    ''' Return ANSI colored text which is unreadable (red text on a red background)

        @param text: the text to be made unreadable
        @type text: string

        @return: the text with ANSI colors
        @rtype: string
    '''
    return "\033[41m\033[31m%s\033[0m" % text



def bold(text):
    ''' Return ANSI colored text which is bold

        @param text: the text to be made bold
        @type text: string

        @return: the text with ANSI colors
        @rtype: string
    '''
    return "\033[1m%s\033[0m" % text



def gray(text):
    ''' Return ANSI colored text which is gray

        @param text: the text to be made gray
        @type text: string

        @return: the text with ANSI colors
        @rtype: string
    '''
    return "\033[30;1m%s\033[0m" % text



def print_if_def(text):
    ''' Return a value if it is defined, or '(undefined)' in gray if it's not

        @param text: the text to be printed
        @type text: string

        @return: the value or undefined text
        @rtype: string
    '''
    if text == None or len(str(text)) == 0:
        return gray("(undefined)      ")
    else:
        return text


def debug(text):
    ''' Print a message is debug is enabled.

        @param text: the text to be printed
        @type text: string
    '''

    global DEBUG
    if DEBUG:
        print "[\033[30;1m***\033[0m] %s" % text



def read_config(configfile):
    ''' Read the configuration from a file

        @param configfile: the configurationfile
        @type configfile: string

        @return: (filename, cryptokey, timeout)
        @rtype: tuple
    '''
    if not os.path.isfile(configfile):
        return (None, None, None)

    try:
        config = ConfigParser.ConfigParser()
        config.read(configfile)
        crypto_key = config.get("token", "crypt")
        filename = config.get("token", "filename")
        timeout = config.get("token", "timeout") 
    except Exception, e:
        debug("failed to read config: %s" % e)
        return (None, None, None)

    return (filename, crypto_key, timeout)



def read_token(configfile):
    ''' Reads a token file based on a given location
        and decrypt it using the info int the configfile.

        The active username is validated against the
        username in the token.

        The timestamp in the token is validated for
        expiry according to the lifetime in the config.
        
        @param configfile: the configfile used
        @type configfile: string

        @return: the decrypted password
        @rtype: string
    '''

    debug("reading configfile %s" % configfile)
    (filename, crypto_key, timeout) = read_config(configfile)
    if filename == None:
        debug("configfile %s not found or not valid." % configfile)
        return None

    debug("reading token at %s/%s" % (os.path.expanduser("~"), filename))
    token = open("%s/%s" % ( os.path.expanduser("~"), filename), "r").readline().strip()

    decrypt = ""

    try:
        rd = Rijndael(crypto_key, block_size=BLOCK_SIZE)
        while len(token) > 0:
            block =  rd.decrypt(token[0:BLOCK_SIZE])
            decrypt = "%s%s" % (decrypt, block)
            token = token[BLOCK_SIZE:]
        
        # strip all NULL padding from the decrypted token
        decrypt = decrypt.replace(chr(0), '')
        (username, pwd, timestamp) = decrypt.split("|")
    except:
        debug("failed to decrypt the token")
        return None
            
    if username != getpass.getuser():
        print "This token does not belong to this user, aborting!"
        sys.exit(1)

    if int(timestamp) + int(timeout) <= time.time():
        print "Token expired."
        debug("Timestamp: %s, timeout: %s, now: %s" % (timestamp, timeout, time.time()))
        return None

    return pwd



def save_token(configfile, username, password):
    '''Save the token to file after encrypting it

        @param configfile: the configfile used
        @type configfile: string

        @param username: the username
        @type username: string

        @param password: the password to be save
        @type password: string
    '''

    try:
        if configfile == None:
            configfile = LOCAL_CONFIG

        if not os.path.isfile(configfile):
            mk_config(configfile)

        (filename, crypto_key, timeout) = read_config(configfile)

        outfile = open("%s/%s" % (os.environ["HOME"], filename), "w")
        r = Rijndael(crypto_key, block_size=BLOCK_SIZE)
        to_crypt = "%s|%s|%d%c" % (username, password, int(time.time()), chr(0))

        encrypted = ""
        while len(to_crypt) > 0:
            block = "%s%s" % (to_crypt[0:BLOCK_SIZE], chr(0) * (BLOCK_SIZE - len(to_crypt)))
            encrypted = "%s%s" % (encrypted, r.encrypt(block))
            to_crypt = to_crypt[BLOCK_SIZE:]


        outfile.write(encrypted)
        outfile.close()

        debug("token saved to %s" % filename)

    except Exception, e:
        print "Failed to update the token at %s\n" % filename
        print e



def mk_config(filename):
    ''' Create a new config file

        @param filename: the filename of the configfile
        @type filename: string
    '''
    if os.path.exists(filename):
        sys.stderr.write("Configfile already exists at %s, not replacing it.\n" % filename)
        return

    random_data = os.urandom(128)
    crypt = hashlib.md5(random_data).hexdigest()[:16]
    try:
        f = open(filename, "w")
        f.write("[token]\nfilename = .teampasswd_token\ntimeout = 7200\ncrypt = %s\n" % crypt)
        f.close()
        debug("created a config file at %s" % filename)
    except Exception, e:
        sys.stderr.write("Failed to create a config at %s\n" % filename)



def json_request(request):
    ''' Do a JSON request to the TeamPasswordManager API 

        @param request: the request to be made (not including the API URL)
        @type request: string

        @return: the result of the query as a JSON object
        @rtype: object
    '''
    global username, password


    tries = 3
    while tries:
        base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
        try:
            req = urllib2.Request("%s/%s"% (API_URL, request), None, 
                { "Content-Type" : "application/json; charset=utf-8"})
            req.add_header("Authorization", "Basic %s" % base64string)
            opener = urllib2.build_opener()
            f = opener.open(req)
            return simplejson.load(f)

        except urllib2.HTTPError, e:
            # print the statuscode if the HTTP request failed and exit
            if e.code == 500:
                tries -= 1
                sys.stderr.write("Failed to contact the server, retrying...\n")
            else:
                sys.stderr.write("ERROR: %s\n" % e)
                sys.exit(1)

        tries -= 1

    sys.stderr.write("Error contacting the server.\n")
    sys.exit(1)



def print_password(pwd, show=False, visible=True):
    ''' Print password details

        @param pwd: the password JSON object to be printed
        @type pwd: JSON pwd object (from TeamPasswordManager)

        @param show: should the password information be shown
        @type show: boolean

        @param visible: should the password be visible. If False, 
                        unreadable text will be printed
        @type visible: boolean
    '''

    if not pwd["locked"]:
        barny = ""
        enable = ""
        for i in range(1,10):
            f = "custom_field{}".format(i)
            if pwd.has_key(f) and pwd[f] != None and pwd.get(f, {}).get("label", "") == "BARNY":
                barny = pwd[f]["data"]
            if pwd.has_key(f) and pwd[f] != None and pwd.get(f, {}).get("label", "") == "enable password":
                enable = pwd[f]["data"]
        print "Name:     {:<25s}     Project:      {:<25s}".format(
            pwd["name"], pwd["project"]["name"])
        print "Manager:  {:<25s}     BARNY:        {:<25s}".format(
            pwd.get("managed_by", {}).get("name","(unknown)"), print_if_def(barny))
        print "Created:  {:<25s}     Last update:  {:<25s}".format(
            pwd.get("created_on", ""), pwd["updated_on"])
        print "Favorite: {:<25s}     Archived:     {:<25s}".format(
            "Yes" if pwd["favorite"] else "No", "Yes" if pwd["archived"] else "No")
        if len(pwd["email"]) > 0:
            print "Email:    {}".format(pwd["email"])
        if len(pwd["tags"]) > 0:
            print "Tags:     {}".format(pwd["tags"])
        print "Details:  {}/pwd/view/{}".format(BASE_URL, pwd["id"])
        if len(pwd["access_info"]) > 0:
            print "Access:   {}".format(pwd["access_info"])

        if not visible:
            print "Username: {:<25s}             Password:     {}".format(
                bold(print_if_def(pwd.get("username"))), invisible(print_if_def(pwd.get("password"))))
            if enable:
                print "                                        Enable:       {:<25s}".format(invisible(enable))

        else:
            print "Username: {:<25s}             Password:     {}".format(
                bold(print_if_def(pwd.get("username"))), bold(print_if_def(pwd.get("password"))))
            if enable:
                print "                                        Enable:       {:<25s}".format(bold(enable))

    else:
        print gray("This password is locked.")



def print_password_list(passwords, show=False, archived=False, visible=False, header=False):
    ''' Print a list of passwords

        @param passwords: a JSON object with a list of TeamPasswordManager password objects
        @type passwords: object

        @param show: show the passwords
        @type show: boolean

        @param archived: include archived passwords
        @type archived: boolean

        @param visible: make the passwords visible
        @type visible: boolean

        @param header: indicate if a header should be printed
        @type header: boolean
    '''

    # force show if visible is set
    if visible:
        show = True

    # print a header if requested
    if header:

        header = " {:>3s}   {:<25s}".format("ID", "Name")
        header2 = "-{}---{}".format("-" * 3, "-" * 25)

        if show:
            header = "{}   {:<25s}   {:<25s}".format(header, "Username", "Password")
            header2 = "{}---{}---{}".format(header2, "-" * 25, "-" * 25)

        header = "{}  ".format(header)
        header2 = "{}-".format(header2)

        print header2
        print header
        print header2

    # do a request for every password to get details
    password = None
    for pwd in passwords:
        if show:
            password =json_request("/passwords/%d.json" % pwd["id"])
        if not pwd["archived"] or archived:
            line = " {:3n}   {:<25s}".format(pwd["id"], pwd["name"])
            if show:
                if not pwd["locked"]:
                    if visible:
                        line = "{}   {:<25s}   {:<25s}".format(line, 
                            pwd.get("username", ""), str(password["password"]))
                    else:
                        line = "{}   {:<25s}   {:<25s}".format(line, 
                            pwd.get("username",""), invisible(password["password"]))
                else:
                    line = "%s   %25s         %25s" % (line, 
                        gray("<<password locked>>"), gray("<<password locked>>"))
            print line



def show_password(id, show=False, only=False, visible=True, clipboard=False, cliptype="primary"):
    ''' The "show" CLI command. This shows the password and
        if requested details for a given password ID.

        @param id: password ID (as used in TeamPasswordManager)
        @type id: integer
        
        @param clipboard: copy the password to the clipboard
        @type clipboard: boolean

        @param only: show only the password, no other info
        @type only: boolean

        @param visible: print the actual password. If set to False 
        an invisible password will be printed.
        @type visible: boolean
    '''

    pwd = json_request("/passwords/%s.json" % id)
    if only and pwd["locked"]:
        print "This password is locked. Please visit {}/pwd/view/{} to unlock it.".format(BASE_URL, pwd["id"])
        sys.exit(1)

    elif only:
        if visible:
            print pwd["password"]
        else:
            print invisible(pwd["password"])
    else:
        print_password(pwd, show, visible)

    if clipboard:
        try:
            proc = Popen(["xclip", "-l", "1", "-selection", cliptype], stdin=PIPE, stderr=PIPE)
            proc.stdin.write(pwd["password"])
            print "\nPassword copied to the clipboard '%s'." % cliptype
        except OSError, e:
            sys.stderr.write("\nFailed to copy the password to the clipboard. Make sure 'xclip' is installed.\n")



def list_passwords(show=False, archived=False, visible=False):
    ''' The "list" CLI command, which shows a list of all passwords
        accessible to the user.

        @param show: include passwords in output
        @type show: boolean

        @param archived: include archived passwords
        @type archived: boolean

        @param visible: print visible passwords. If set to false,
        invisible passwords will be printed.
        @type visible: boolean
    '''

    count = json_request("/passwords/count.json")
    for page in range(1, count["num_pages"] +1):
        passwords = json_request("/passwords/page/%d.json" % page)
        print_password_list(passwords, show=show, archived=archived, 
                            visible=visible, header = (page == 1))

    print "\n%d password%s in list." % (
        count["num_items"], "s" if count["num_items"] > 1 else "")



def search_passwords(query, show=False, visible=False):
    ''' The "search" CLI command, which shows a list of all passwords
        accessible to the user.

        @param query: the search query
        @type query: string

        @param show: include passwords in output
        @type show: boolean

        @param visible: print visible passwords. If set to false,
        invisible passwords will be printed.
        @type visible: boolean
    '''
    
    count = json_request("/passwords/search/%s/count.json" % urllib.quote(query))
    if count["num_items"] == 1:
        passwords = json_request("/passwords/search/%s/page/1.json" % 
            urllib.quote(query))
        show_password(passwords[0]["id"], show=show, visible=visible)
    else:
        for page in range(1, count["num_pages"] +1):
            passwords = json_request("/passwords/search/%s/page/%d.json" % 
                (urllib.quote(query), page))
            print_password_list(passwords, show=show, visible=visible, header = (page == 1))
        print "\n%d password%s matched." % (
            count["num_items"], "s" if count["num_items"] > 1 else "")



def dump_password(id):
    ''' The "dump" CLI command. This shows the password as a raw JSON object.

        @param id: password ID (as used in TeamPasswordManager)
        @type id: integer
    '''
    
    pwd = json_request("/passwords/%s.json" % id)
    print simplejson.dumps(pwd, sort_keys=True, indent='    ')



def fav_passwords(show=False, visible=True):
    ''' The "favorites" CLI command, which shows a list of passwords marked
        as favorite.

        @param show: include passwords in output
        @type show: boolean

        @param visible: print visible passwords. If set to false,
        invisible passwords will be printed.
        @type visible: boolean
    '''
    
    count = json_request("/passwords/favorite/count.json")
    for page in range(1, count["num_pages"] +1):
        favs = json_request("/passwords/favorite/page/%d.json" % page)
        print_password_list(favs, show=show, archived=True, visible=visible, 
            header = (page == 1))
    print "%d password%s in favorites." % (
        count["num_items"], "s" if count["num_items"] > 1 else "")



if __name__ == "__main__":
    try:
        parser = argparse.ArgumentParser()
        parser.add_argument(
            "-d", "--debug", 
            default=False, 
            action="store_true", 
            help="Debugging on", 
            dest="debug"
        )

        subparsers = parser.add_subparsers(help="commands", dest="command")

        # ----- list command -----
        list_parser = subparsers.add_parser("list", help="List all passwords")
        list_parser.add_argument(
            "-a", "--show-archived", 
            action="store_true", 
            default=False, 
            help="Show archived passwords", 
            dest="archived"
        )
        list_parser.add_argument(
            "-s", "--show-passwords", 
            action="store_true", 
            default=False, 
            help="Show actual passwords", 
            dest="show"
        )
        list_parser.add_argument(
            "-v", "--visible", 
            action="store_true", 
            default=False, 
            help="Make the password visible", 
            dest="visible"
        )

        # ----- show command -----
        show_parser = subparsers.add_parser("show", help="Show a password")
        show_parser.add_argument(
            "id", 
            action="store", 
            help="Unique password ID", 
            type=int
        )
        show_parser.add_argument(
            "-c", "--clipboard", 
            default=False, 
            action="store_true", 
            help="Copy password to clipboard", 
            dest="clipboard"
        )
        show_parser.add_argument(
            "-o", "--password-only", 
            default=False, 
            action="store_true", 
            help="Show only the stored password", 
            dest="only"
        )
        show_parser.add_argument(
            "-t", "--cliptype", 
            action="store", 
            dest="cliptype", 
            choices=("primary", "secondary", "clipboard", "buffer-cut"),
            help="output clipboard type",
            default="primary"
        )
        show_parser.add_argument(
            "-v", "--visible", 
            default=False, 
            action="store_true", 
            help="Make the password visible", 
            dest="visible"
        )
    
        # ----- search command -----
        search_parser = subparsers.add_parser("search", help="Search for passwords")
        search_parser.add_argument(
            "query", 
            action="store", 
            help="The search query"
        )
        search_parser.add_argument(
            "-s", "--show-passwords", 
            action="store_true", 
            default=False, 
            help="Show actual passwords", 
            dest="show"
        )
        search_parser.add_argument(
            "-v", "--visible",
            action="store_true", 
            default=False, 
            help="Make the password visible", 
            dest="visible"
        )

        # ----- favorites command -----
        fav_parser = subparsers.add_parser("favorites", help="Show favorite passwords")
        fav_parser.add_argument(
            "-s", "--show-passwords", 
            action="store_true", 
            default=False, 
            help="Show the actual passwords", 
            dest="show"
        )
        fav_parser.add_argument(
            "-v", "--visible", 
            action="store_true", 
            default=False, 
            help="Make the password visible", 
            dest="visible"
        )

        # ----- dump command -----
        dump_parser = subparsers.add_parser("dump", help="Show a password as a raw JSON object")
        dump_parser.add_argument(
            "id", 
            action="store", 
            help="Unique password ID", 
            type=int
        )

        # ----- version command -----
        version_parser = subparsers.add_parser("version", help="Show version info")

        args = parser.parse_args()

        if args.debug:
            DEBUG = True
            debug("debugging on")

        if args.command == "version":
            print "$Id: teampasswd 464 2016-07-14 11:27:39Z sanders $\n"
            sys.exit(0)
  
        # try to read the BACE config and use that to retrieve a user's 
        # BACE password from his BACE token if it's still valid.

        cfg = BACE_CONFIG
        username = getpass.getuser()
        password = None

        password = read_token(BACE_CONFIG)
        if password == None:
            # no BACE token found, let's try a local token
            cfg = LOCAL_CONFIG
            password = read_token(LOCAL_CONFIG)

        # no password found in the token, let's ask for one
        if password == None:
            #  ask the password
            password = getpass.getpass("Please enter your password: ")

        if args.command == "list":
            list_passwords(show=args.show, archived=args.archived, visible=args.visible)

        elif args.command == "show":
            show_password(id=args.id, only=args.only, visible=args.visible, 
                clipboard=args.clipboard, cliptype=args.cliptype)

        elif args.command == "search":
            search_passwords(args.query, show=args.show, visible=args.visible)

        elif args.command == "favorites":
            fav_passwords(show=args.show, visible=args.visible)

        elif args.command == "dump":
            dump_password(id=args.id)


        save_token(cfg, username, password)


    except KeyboardInterrupt:
        print "\nAborted.\n"
        sys.exit(1)

