#!/usr/bin/perl
# gpgpwd
# Copyright (C) Eskild Hustvedt 2012, 2013, 2014
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use warnings;
use 5.010;
use Getopt::Long;
use JSON qw(encode_json decode_json);
use Try::Tiny;
use IPC::Open2 qw(open2);
use IPC::Open3 qw(open3);
use MIME::Base64 qw(encode_base64 decode_base64);
use IO::Select;
use File::Copy qw(move copy);
use File::Basename qw(basename dirname);
use Term::ReadLine;
use Cwd qw(getcwd realpath);
use constant {
    true => 1,
    false => undef,

    V_INFO => 1,
    V_LOG => 2,
    V_DEBUG => 3,
};

my $VERSION          = '0.5';
my @gpg              = qw(gpg --gnupg --default-recipient-self --no-verbose --quiet --personal-compress-preferences uncompressed);
my $storagePath      = $ENV{HOME}.'/.gpgpwddb';
my $dataVersion      = 2;
my $enableGit        = false;
my $useXclip         = true;
my $verbosity        = 0;
my $allMatches       = false;
my $autoGPGAgent;
my @clipboardTargets = qw(clipboard);
my $pDieUsepExit     = 1;

# Purpose: print() wrapper that obeys $verbosity
# Usage: printv(LEVEL, ...);
# LEVEL is one of the V_* constants, ... are the normal print() parameters
sub printv
{
    my $level = shift;
    if ($verbosity >= $level)
    {
        my @prefixes;
        $prefixes[V_INFO]  = 'info';
        $prefixes[V_LOG]   = 'log';
        $prefixes[V_DEBUG] = 'debug';
        printf('[gpgpwd %-5s] ',$prefixes[$level]);
        print @_;
    }
}

# Purpose: Outputs a status message during initialization
# Usage: statusOut(message)
#
# This is silent if verbosity is 0
sub statusOut
{
    # Ignore status messages if verbosity != 0
    if ($verbosity == 0)
    {
        my $message = shift;
        state $prevLength = 0;
        # Clear the previous message
        print "\r";
        for(my $i = $prevLength; $i > 0;$i--)
        {
            print ' ';
        }
        # Output the new message
        print "\r$message";
        $prevLength = length($message);
    }
}

# Purpose: system() wrapper that outputs the command if $verbosity >= V_LOG
# Usage: Same as system()
sub psystem
{
    if ($verbosity >= V_LOG)
    {
        printv(V_LOG,'Running: '.join(' ',@_)."\n");
    }
    return system(@_);
}

# Purpose: Wrapper around die() that uses pExit
# Usage: Same as die()
sub pDie
{
    if (!$pDieUsepExit)
    {
        die(@_);
    }
    warn(@_);
    pExit(254);
}

# Purpose: Wrapper around exit that restores backups if needed
# Usage: Same as exit()
sub pExit
{
    my $ret = shift;
    # Gently kill our started gpg agent if needed
    if (defined($autoGPGAgent) && $autoGPGAgent > -1)
    {
        my @GPGInfo = split(':',$ENV{GPG_AGENT_INFO});
        kill('SIGINT',$GPGInfo[1]);
        printv(V_DEBUG,'Killed our gpg agent ('.$GPGInfo[1].')'."\n");
    }
    if (-e $storagePath.'~' && ! -e $storagePath)
    {
        print 'Note: while exiting '.$storagePath.'~ existed, but '.$storagePath.' did not.'."\n";
        print 'Restored backup copy to '.$storagePath."\n";
        move($storagePath.'~',$storagePath);
    }
    exit($ret);
}

# Purpose: Check for a file in path
# Usage: InPath(FILE)
sub InPath
{
    foreach (split ':', $ENV{PATH}) { if (-x "$_/@_" and ! -d "$_/@_" ) {    return "$_/@_"; } } return false;
}

# Purpose: Copy a string to the clipboard if possible
# Usage: $info = toClipboard(VALUE);
# VALUE is the value to copy to the clipboard
# toClipboard returns a string, which is empty if nothing was done and '
# (copied)' if something was copied to the clipboard.
sub toClipboard
{
    my $value        = shift;
    if (!$useXclip)
    {
        return '';
    }
    if (!InPath('xclip') || !(defined($ENV{DISPLAY}) && length($ENV{DISPLAY})))
    {
        if(InPath('xclip'))
        {
            printv(V_LOG,'Use of xclip disabled: no DISPLAY set'."\n");
        }
        else
        {
            printv(V_LOG,'Use of xclip disabled: not installed'."\n");
        }
        return '';
    }
    foreach my $target (@clipboardTargets)
    {
        open(my $write,'|-','xclip','-in','-selection',$target,'-silent') or return '';
        print {$write} $value or return '';
        close($write) or printv(V_INFO,'Failed to close filehandle to xclip: '.$!."\n");
        printv(V_DEBUG,'xclip instance started for '.$target."\n");
    }
    return ' (copied)';
}

# Purpose: Get a random password
# Usage: $randomPwd = randomPwd(length = 15, alphaNumOnly = false);
sub randomPwd
{
    my $length = shift;
    my $alphaNumOnly = shift;
    $length  //= 15;
    while(1)
    {
        my $pwd = '';
        # These characters are chosen specifically because they are usually selectable in
        # a terminal by simply double-clicking on the string.
        my @chars = ('a'..'z','A'..'Z',0..9);
        if (!$alphaNumOnly)
        {
            push(@chars,',','.','/','?','%','&','#',':','_','=','+','@','~');
        }
        while(length($pwd) < $length)
        {
            $pwd .= $chars[ rand scalar @chars ];
        }
        # Require a password to have at least one number, one lower- and one
        # upper-case character, and one non-word (symbol) character
        if ($pwd =~ /\d+/ && $pwd =~ /[A-Z]/ && $pwd =~ /[a-z]/ && ($alphaNumOnly || $pwd =~ /\W/))
        {
            return $pwd;
        }
        # Password not accepted, try again
    }
}

# Purpose: Check for gpg-agent and start one if needed
# Usage: autoInitGPGAgent()
sub autoInitGPGAgent
{
    if (defined($autoGPGAgent))
    {
        return;
    }
    elsif(!gpgAgentRunning())
    {
        if(InPath('gpg-agent'))
        {
            printv(V_DEBUG,'Autostarting gpg-agent'."\n");
            # Start the agent
            $autoGPGAgent = open2(my $out, my $in, qw(gpg-agent --daemon));
            # Read the first line containing the GPG_AGENT_INFO variable
            my $info = <$out>;
            # Parse out the variable contents
            $info =~ s/GPG_AGENT_INFO=(\S+); export GPG_AGENT_INFO/$1/;
            chomp($info);
            # Set the GPG_AGENT_INFO environment variable for gpg to use
            $ENV{GPG_AGENT_INFO} = $info;
            printv(V_DEBUG,'gpg-agent['.$autoGPGAgent.'] started and listening at '.$info."\n");
            # Disable tty
            printv(V_DEBUG,'Enabling agent requirement (--no-tty)'."\n");
            push(@gpg,'--no-tty');
            # Explicitly enable the agent for gpg1
            if ($gpg[0] ne 'gpg2')
            {
                push(@gpg,'--use-agent');
            }
        }
        else
        {
            # If we have no gpg-agent then the user will get tired of typing
            # their password fast, so output a message on how to avoid that.
            print "NOTICE: No gpg-agent available. Install gpg-agent to avoid unneccesary\npassword prompts\n";
            $autoGPGAgent = -1;
        }
    }
    elsif (!defined($autoGPGAgent))
    {
        printv(V_DEBUG,'Not autostarting gpg-agent: already appears to be running'."\n");
        $autoGPGAgent = -1;
    }
}

# Purpose: Check if a gpg agent is running
# Usage: bool = gpgAgentRunning()
sub gpgAgentRunning
{
    if (defined($ENV{GPG_AGENT_INFO}) && length($ENV{GPG_AGENT_INFO}))
    {
        my @GPGInfo   = split(':',$ENV{GPG_AGENT_INFO});
        if (-e $GPGInfo[0] && kill(0,$GPGInfo[1]))
        {
            return 1;
        }
    }
    return;
}

# Purpose: Output a message to stderr or die, restoring STDERR first
# Usage: reOut($stderr,die?,message);
sub reOut
{
    my $stderr = shift;
    my $die = shift;
    my $message = shift;

    open(my $storedErr,'>&',\*STDERR);
    open(STDERR,'>&',$stderr);
    if ($die)
    {
        pDie($message);
    }
    else
    {
        warn($message);
    }
    open(STDERR,'>&',$storedErr);
}

# Purpose: Write some data to gpg and return the output from gpg
# Usage: gpgIO(data,[ gpgCommands ], requiresUnlocked = false, captureSTDERR = false)
# data is the data to be written to gpg
# gpgCommands is an arrayref of parameters for gpg
# requiresUnlocked is a boolean. Set it to true if the provided gpgCommands require
#   the key to be unlocked
# captureSTDERR is a boolean. Set it to true to capture both stdout and stderr
sub gpgIO
{
    my $data             = shift;
    my $commandList      = shift;
    my $requiresUnlocked = shift;
    my $captureSTDERR    = shift;
    my @commands         = @{$commandList};

    my $output;
    my $stderr;
    if (!$captureSTDERR)
    {
        open($stderr, '>&',\*STDERR);
        # This is used to silence useless warnings from gnupg when using the
        # gnome keyring daemon. It's not safe to do unless we are under gnome
        # and have a DISPLAY. If we don't then we risk killing password
        # prompts.
        if(defined($ENV{DISPLAY}) && defined($ENV{XDG_CURRENT_DESKTOP}) && $ENV{XDG_CURRENT_DESKTOP} eq 'GNOME' && $verbosity <= 0)
        {
            open(STDERR,'>','/dev/null');
        }
    }

    # Autostart a gpg-agent for us if needed
    autoInitGPGAgent();

    # Declare I/O variables
    my($in,$out,$err,$pid,$outputReader);

    # Generate the complete gpg commandline
    my @gpgCommand = @gpg;
    push(@gpgCommand,@commands);

    # If we're not requiring an agent then gpg will output some messages. To
    # differenciate it from "our" output, encase it in a text block.
    if (!gpgAgentRunning() && $requiresUnlocked)
    {
        print '-- gpg --'."\n";
    }
    # Use open3 to capture stdout+stderr
    if ($captureSTDERR)
    {
        printv(V_LOG,'Talking to gpg with open3: '.join(' ',@gpgCommand)."\n");
        $pid = open3($in,$out,$err,@gpgCommand) or reOut($stderr,true,'Failed to open3() up communication with gpg: '.$!."\n");
        # Create an IO::Select handle that we use to query for pending data
        # on the STDOUT and STDERR filehandles
        $outputReader = IO::Select->new($out,$err);
    }
    # Use open2 to capture stdout
    elsif(defined($data))
    {
        printv(V_LOG,'Talking to gpg with open2: '.join(' ',@gpgCommand)."\n");
        $pid = open2($out,$in,@gpgCommand) or reOut($stderr,true,'Failed to open2() up communication with gpg: '.$!."\n");
        # Create an IO::Select handle that we use to query for pending data
        # on the STDOUT filehandle
        $outputReader = IO::Select->new($out);
    }
    else
    {
        printv(V_LOG,'Talking to gpg with open: '.join(' ',@gpgCommand)."\n");
        $pid = open($out,'-|',@gpgCommand) or pDie('Failed to open() up communication with gpg: '.$!."\n");
        # Create an IO::Select handle that we use to query for pending data
        # on the STDOUT filehandle
        $outputReader = IO::Select->new($out);
    }
    # If we have data and it is an arrayref, print it line-by-line
    if(defined($data) && ref($data) eq 'ARRAY')
    {
        foreach my $l (@{$data})
        {
            print {$in} $l."\n" or reOut($stderr,true,'Failed to write data to gpg for output: '.$!."\n");
            # Check if there is pending output data that we need to read
            while(my @handles = $outputReader->can_read(0))
            {
                foreach my $handle (@handles)
                {
                    $output .= <$handle>;
                }
            }
        }
    }
    # Dump the entire data string if we have one
    elsif(defined($data))
    {
        print {$in} $data or reOut($stderr,true,'Failed to write data to gpg for output: '.$!."\n");
    }
    # Close the input filehandle
    if(defined($in))
    {
        close($in) or reOut($stderr,false,'Failed to close communication with gpg: '.$!."\n");
    }
    # Read all pending output
    while(my @handles = $outputReader->can_read(120))
    {
        foreach my $handle (@handles)
        {
            my $read = false;
            while(my $input = <$handle>)
            {
                $read = true;
                $output .= $input;
            }
            if (!$read)
            {
                $outputReader->remove($handle);
                last;
            }
        }
    }

    if (!$captureSTDERR)
    {
        open(STDERR,'>&',$stderr);
    }

    # Close the output handle
    close($out) or warn('Failed to close communication with gpg: '.$!."\n");
    # Close the stderr handle if needed
    if(defined($err))
    {
        close($err) or warn('Failed to close communication with gpg: '.$!."\n");
    }
    # If we're not requiring an agent then gpg will output some messages. To
    # differenciate it from "our" output, encase it in a text block.
    if (!gpgAgentRunning() && $requiresUnlocked)
    {
        print '-- --- --'."\n";
    }
    # Wait for children (ie. gpg) to exit, to avoid defunct processes
    waitpid($pid,0);

    # Return whatever output we got from gpg
    return $output;
}

# Purpose: Decrypt a string with gpg and return it
# Usage: gpgDecryptString(STRING, BASE64=false)
# STRING is the string to encrypt
# BASE64 bool, if true the returned decrypted data will be base64-deencoded
#   before being returned
sub gpgDecryptString
{
    my $string = shift;
    my $base64 = shift;
    # Decode the base64 string if needed
    if ($base64)
    {
        try
        {
            $string = decode_base64($string);
        }
        catch
        {
            pDie('Failed to decode BASE64: '.$_."\n");
        }
    }

    # Decrypt the string
    my $output = gpgIO($string,[ qw(--decrypt) ],true);

    # Return the decrypted data
    return $output;
}

# Purpose: Encrypt a string with gpg and return it
# Usage: gpgEncryptString(STRING, BASE64=false)
# STRING is the string to encrypt
# BASE64 bool, if true the returned encrypted data will be base64-encoded
#   before being returned
sub gpgEncryptString
{
    my $string = shift;
    my $base64 = shift;

    # Encrypt the data
    my $output = gpgIO($string,[ qw(--encrypt) ]);
    # Return base64 encoded data if needed
    if ($base64)
    {
        return encode_base64($output,'');
    }
    # Return raw data
    return $output;
}

# Purpose: Verify metadata signature
# Usage: verifyMetadataSignature(PATH,Data)
sub verifyMetadataSignature
{
    my $path = shift;
    my $data = shift;

    printv(V_LOG,'Verifying metadata signature'."\n");

    # Reset LC_ALL to C
    my $LC_ALL = $ENV{LC_ALL};
    $ENV{LC_ALL} = 'C';

    # Retrieve which pubkey the database is encrypted with
    my $pubkey = gpgIO(undef,[ qw(--verbose --decrypt --list-only), $path ],false,true);
    # If we didn't get a list then something went wrong
    if (!defined($pubkey))
    {
        pDie('Signature validation failed: unable to retrieve encryption pubkey from database file'."\n");
    }
    chomp($pubkey);
    # Parse out the public key
    $pubkey =~ s/gpg: public key is\s+//;
    # If there are spaces in it then the regex above appears to have failed and we're
    # left with an unusable string
    if ($pubkey =~ /\s/)
    {
        printv(V_DEBUG,'Public key parsed to be "'.$pubkey.'"'."\n");
        pDie('Signature validation failed: unable to parse out the pubkey from gpg output'."\n");
    }

    # Verify the signature
    my $signature = gpgIO($data,[ qw(--verify) ],false,true);

    # If no data was returned then something went wrong with gpg
    if (!defined($signature))
    {
        pDie('Signature validation failed: gpg did not return any response about signature validity'."\n");
    }

    # If it does not contain this text, then the validation failed
    if ($signature !~ /^gpg: Good signature from/m)
    {
        pDie('Signature validation failed: invalid signature'."\n");
    }

    printv(V_DEBUG,'gpg signature validation step succeeded'."\n");

    # Parse out the signature key
    my $sigKey = $signature;
    $sigKey =~ s/.*gpg: Signature made .*? using \S+ key ID (\S+).*/$1/s;
    # If there are still spaces left then we failed to parse the key
    if ($sigKey =~ /\s/)
    {
        pDie('Signature validation failed: unable to parse out the sigkey from gpg output'."\n");
    }

    printv(V_DEBUG,'signing key is '.$sigKey.', pubkey for encryption is '.$pubkey."\n");

    # Retrieve a list of all signatures matching the pubkey
    my $keyList = gpgIO(undef, [ qw(--with-colons --list-sigs), $pubkey ]);
    # If no data was returned then something went wrong with gpg
    if(!defined($keyList))
    {
        pDie('Signature validation failed: Unable to retrieve signature list from gpg'."\n");
    }
    my $valid = false;
    my $sigKeyLen = length($sigKey);
    # Iterate through all lines returned from gpg
    foreach my $line(split("\n",$keyList))
    {
        # The content is :-separated, split it into an array
        my @content = split(':',$line);
        # We only care about 'sig' lines
        if ($content[0] eq 'sig')
        {
            # If the line contains the key, and the key is at the end of the line
            # (which is determined through the length of the line minus the length of the
            # key, index() will return at which point $sigKeyLen starts) then it is the
            # same key, and the validation has succeeded.
            if (index($content[4],$sigKey) == ( length($content[4]) - $sigKeyLen ) )
            {
                $valid = true;
                last;
            }
        }
    }

    # Error out if we have no valid signature
    if (!$valid)
    {
        pDie('Signature validation failed: Signature does not match encryption key'."\n");
    }

    # Switch LC_ALL back to the user value
    $ENV{LC_ALL} = $LC_ALL;
}

# Purpose: Retrieve the JSON structure stored in the password database
# Usage: ($preV2,$structure) = loadJSONStructure(PATH);
# NOTE: In practically all circumstances you want to call loadData() and NOT
#   loadJSONStructure. loadData performs validation, upgrades, safety checks
#   and so on which this function does not.
sub loadJSONStructure
{
    my $path = shift;
    if (!-e $path)
    {
        return (0, {
            gpgpwdDataVersion => $dataVersion,
            pwds => {},
        });
    }

    my $preV2  = true;
    my $string = gpgIO(undef,[ '--decrypt',$path ]);
    if (!defined($string))
    {
        pDie('Decryption failed: GPG did not return any data'."\n");
    }

    # If we are using v2+ data format, then we need to perform additional parsing
    if (index($string,'-----BEGIN PGP SIGNED MESSAGE-----') == 0)
    {
        # We're not using a pre V2 data format
        $preV2 = false;
        # The JSON string will be stored here
        my $jsonString = '';
        my $seenStart  = false;
        # Iterate through all lines to parse out the JSON string
        foreach my $l (split("\n",$string))
        {
            # A line starting with { indicates the beginning of the JSON string
            if (!$seenStart && (index($l,'{') == 0))
            {
                $seenStart = true;
            }
            # The PGP SIGNATURE line indicates the end of the JSON string
            elsif (index($l,'-----BEGIN PGP SIGNATURE-----') == 0)
            {
                last;
            }
            # If we have seen the start (ie. the initial {), treat this as a JSON string
            if ($seenStart)
            {
                $jsonString .= $l;
            }
        }
        # Verify the GPG signature of the metadata
        verifyMetadataSignature($path,$string);
        # Replace the raw string with the parsed JSON string
        $string = $jsonString;
    }

    my $data;

    # Decode the JSON
    try
    {
        $data = decode_json($string);
    }
    catch
    {
        pDie('Failed to decode encrypted JSON data. The file is either not a gpgpwd'."\n".
            'file, or the file is corrupt.'."\n".
            'JSON error: '.$_."\n\n".
            'If the file is a corrupt gpgpwd file, you may be able to recover it by'."\n".
            'manually decrypting the file and then editing it.'."\n");
    };
    return ($preV2,$data);
}

# Purpose: Upgrade a v1 database file to v2
# Usage: upgradeDataV1toV2();
sub upgradeDataV1toV2
{
    # Load the data
    my($preV2,$data) = loadJSONStructure($storagePath);
    # Check if the data has already been upgraded
    if($data->{gpgpwdDataVersion} eq '2')
    {
        # If it is a preV2 file then someone has tampered with it
        if ($preV2)
        {
            pDie('Your database file is a v1 file claiming to be v2'."\n".
                'Someone has tampered with your database file. Upgrade aborted.'."\n");
        }
        else
        {
            print 'Your database has already been upgraded.'."\n";
            pExit(0);
        }
    }
    # If we have seen V2+ data, but the file claims to be version 1 then
    # something is corrupt, so we abort to avoid performing multiple
    # conversions.
    if ($preV2 == 0)
    {
        pDie('Your password database is a v2 file calming to be v1'."\n".
            'The password database may be corrupt. Upgrade aborted.'."\n");
    }
    # First perform a git pull
    if ($enableGit)
    {
        print "First performing a git pull...\n";
        git('pull',$storagePath);
        print "Reloading data from git...\n";
        ($preV2,$data) = loadJSONStructure($storagePath);
        if (!$preV2)
        {
            print "Data in git has been upgraded. Upgrade aborted\n";
            pExit(0);
        }
    }
    print 'Converting ...';
    # Perform data conversion. Encrypts every password explicitly.
    foreach my $pwd (keys %{$data->{pwds}})
    {
        $data->{pwds}->{$pwd} = gpgEncryptString($data->{pwds}->{$pwd},true);
    }
    print "done\n";
    # Write out the data file with the converted data
    print 'Writing updated data...';
    # First, make a backup
    if (-e $storagePath.'.gpgpwdupgrade')
    {
        pDie($storagePath.'.gpgpwdupgrade already exists. Move this file out of the way.'."\n");
    }
    copy($storagePath,$storagePath.'.gpgpwdupgrade') or pDie('Error: Unable to create backup file - '.$!."\n".'Upgrade aborted.'."\n");
    # Write the data
    writeData($storagePath,$data);
    print "done\nVerifying integrity...";
    # Verify the integrity of the upgraded data file
    try
    {
        # Check that the new data is larger than the old
        if (-s $storagePath <= -s $storagePath.'.gpgpwdupgrade')
        {
            die('The old data file is larger than the new. This should not be possible.');
        }
        # Make pDie call die() directly instead of warn()+pExit()
        $pDieUsepExit = 0;
        # Temporarily reduce verbosity to avoid loadData() outputting
        # status information
        my $origVerbosity = $verbosity;
        $verbosity = -1;
        # Load the data, storing errors in $error
        my $error;
        try
        {
            loadData($storagePath);
        }
        catch
        {
            $error = $_;
        };
        # Reset pDie to its default mode
        $pDieUsepExit = 1;
        # Reset verbosity to its default or user-provided value
        $verbosity = $origVerbosity;
        # If the loadData failed, we error out
        if (defined $error)
        {
            die('Failed to load the upgraded file: '.$error);
        }
    }
    catch
    {
        # Restore the backup file
        print "failed\n";
        unlink($storagePath);
        move($storagePath.'.gpgpwdupgrade',$storagePath);
        # Push the restored file if needed
        git('push',$storagePath);
        # Make sure pDie is set to its default mode
        $pDieUsepExit = 1;
        # Error out
        pDie('Error: '.$_.
            'Restoring the old file and aborting the upgrade to avoid corruption.'."\n".
            'This is likely a bug in gpgpwd.'."\n"
        );
    };
    # Remove the backup
    unlink($storagePath.'.gpgpwdupgrade');
    print "done - upgrade successfully completed\n";
    pExit(0);
}

# Purpose: Load the password database
# Usage: $data = loadData(PATH);
# PATH is the path to the location of the database
#
# If PATH does not exist then it will return an empty (but usable)
# $data ref.
sub loadData
{
    my $path         = shift;
    statusOut('(loading password database)');
    my($preV2,$data) = loadJSONStructure($path);

    if (!defined $data->{pwds} || ref($data->{pwds}) ne 'HASH')
    {
        pDie('Detected possible corruption in '.$path.' - refusing to continue'."\n");
    }
    elsif(! defined($data->{gpgpwdDataVersion}))
    {
        pDie($path.': does not specify data format version - refusing to continue'."\n");
    }
    elsif($data->{gpgpwdDataVersion} eq '1')
    {
        statusOut('');
        if ($enableGit)
        {
            print "Old data format detected, performing a git pull...\n";
            git('pull',$storagePath);
            print "Reloading data from git...\n";
            ($preV2,$data) = loadJSONStructure($path);
            if (!$preV2)
            {
                print "Data in git has been upgraded, continuing...\n";
                return loadData($path);
            }
        }
        pDie('Your database file is using the old v1 format. It needs to be upgraded.'."\n".
            'If you have already upgraded your database, then this means someone has '."\n".
            'tampered with your file and the database may be compromised.'."\n\n".
            'To upgrade the database run: gpgpwd upgrade'."\n"
        );
    }
    elsif($data->{gpgpwdDataVersion} ne '1' && $preV2)
    {
        # A data format v1 file is claiming to be a version 2 file. Abort.
        pDie('pre-2 dataformat claiming to be 2+. Someone has modified your password file. Aborting.'."\n");
    }
    elsif ($data->{gpgpwdDataVersion} ne $dataVersion)
    {
        pDie($path.' is version '.$data->{gpgpwdDataVersion}.' of the gpgpwd file format'."\n".
            'This version only supports version '.$dataVersion.' - refusing to continue'."\n");
    }

    $data->{pwds} //= {};

    return $data;
}

# Purpose: Write the password database
# Usage: writeData(PATH,$data);
# PATH is the path to the location to write to
# $data is the data hashref
sub writeData
{
    my $path    = shift;
    my $content = shift;

    my $encoded;

    $content->{generator} = 'gpgpwd '.$VERSION.' - http://random.zerodogg.org/gpgpwd';
    $content->{lastVersion} = $VERSION;
    $content->{gpgpwdDataVersion} = $dataVersion;

    try
    {
        $encoded = encode_json($content);
    }
    catch
    {
        pDie('Failed to encode data for JSON output. This is a bug!'."\n".
            'JSON error: '.$_."\n");
    };
    $encoded =~ s/,/,\n/g;
    my @encodedSplit = split("\n",$encoded);

    # Sign the JSON string. The comment contains quick instructions on decrypting
    # the embedded passwords.
    $encoded = gpgIO(\@encodedSplit,[ qw(--clearsign --comment),'gpgpwd password file. Each password is gpg encrypted, so you will need to decrypt each password you want to look up manually: echo PASSWORD-STRING|gpg -d' ], 1);

    # Make a backup of the current file
    if (-e $path)
    {
        move($path,$path.'~') or pDie('Failed to create backup file: '.$!."\n".'Refusing to write data to avoid loss of previous data'."\n");
    }

    # Write the JSON string encrypted to the supplied file
    gpgIO($encoded,[ '--encrypt','--output',$path ]);
    chmod(0600,$path);

    # Perform some paranoid sanity checks. Verify that the file we just wrote
    # is actually there and that it is above the minimum size of 1468 bytes.
    if (!-e $path)
    {
        pDie('Fatal error: "'.$path.'" did not exist after writing the file'."\n");
    }
    elsif(-s $path < 1468)
    {
        pDie('Fatal error: the size of "'.$path.'" is too small'."\n");
    }

    unlink($path.'~');

    git('push',$path);
}

# Purpose: Load a list of passwords from a simple file
# Usage: loadFromFile(FILE,$data);
# $data is the data hashref
# FILE is the path to the file to read
sub loadFromFile
{
    my $file = shift;
    my $data = shift;

    my $line = 0;
    my $read = 0;
    open(my $in,'<',$file) or pDie('Failed to open '.$file.' for reading: '.$!."\n");
    while(<$in>)
    {
        $line++;

        chomp;

        next if !/\S/;
        next if !length($line);
        next if /^#/;

        my $name = $_;
        my $pwd = $_;

        $name =~ s/^(\S+)\s+.*/$1/;
        $pwd =~ s/^\S+\s+//;

        if (!length($name) || !length($pwd) || $name eq $_ || $pwd eq $_)
        {
            pDie('Failed to parse line '.$line.' in '.$file."\n");
        }
        if(defined $data->{pwds}->{$name})
        {
            my $decryptedPW = gpgDecryptString($data->{pwds}->{$name},true);
            if ($decryptedPW ne $pwd)
            {
                print 'Changed '.$name.' from '.$decryptedPW.' to '.$pwd."\n";
            }
        }
        $read++;
        $data->{pwds}->{$name} = gpgEncryptString($pwd,true);
    }
    # Failing to close is not much of a problem, so just log it.
    close($in) or printv(V_INFO,'Failed to close filehandle: '.$!."\n");;
    print 'Read '.$read.' entries from '.$file."\n";
}

# Purpose: Retrieve a simplistic "file ID" string for a file
# Usage: id = getFileID(FILE);
# The ID string is a very simple string combining the file size and
# file change time.
sub getFileID
{
    my $file = shift;
    if (! -e $file || ! -r $file)
    {
        pDie('Unable to read '.$file."\n");
    }
    my $ID = -s $file;
    $ID .= '|';
    $ID .= (stat($file))[9];
    return $ID;
}

# Purpose: Set a password value in the database
# Usage: set($data,NAME);
# $data is the data hashref
# NAME is the name of the entry to add
sub set
{
    my $data = shift;
    my $name = shift;

    my ($password, $copied);
    my $alphaNumeric = 0;
    my $defaultLength = 15;
    my $length = $defaultLength;
    my $readLine = Term::ReadLine->new('gpgpwd');
    my $existing;
    if(defined $data->{pwds}->{$name})
    {
        $existing = gpgDecryptString($data->{pwds}->{$name},true);
    }
    statusOut('');

    # Loop to retreive the password
    while(1)
    {
        # If $password is defined then this is a second-or-later iteration
        # through the loop
        if(defined $password)
        {
            print "\n";
        }
        my $prompt = 'Password> ';
        # Generate a random password
        my $random = randomPwd($length,$alphaNumeric);
        # Copy it to the clipboard if needed
        $copied = toClipboard($random);
        if (defined $existing)
        {
            print 'An entry for '.$name.' already exists, with the password: '.$existing."\n";
            print 'Enter the password you want to change it to, or press enter to use the random'."\n";
        }
        else
        {
            print 'Enter the password you want to use, or press enter to use the random'."\n";
        }
        print 'password listed below. Some commands are available, enter /help to list them'."\n";
        print 'Random password: '.$random.$copied."\n";
        $password = $readLine->readline('Password> ');
        # If a user sends EOF, just go through the loop again
        if (!defined $password)
        {
            print "\n";
            next;
        }
        chomp $password;
        if (!length $password)
        {
            $password = $random;
            if (!defined $existing)
            {
                print "Using password: $random\n";
            }
            last;
        }
        # Output help text
        elsif(index($password,'/help') == 0)
        {
            print "\n";
            print "The following commands are available:\n";
            printHelp('','/help','Display this help screen');
            printHelp('','/alphanumeric','Generate an alphanumeric password (a password with only letters and numbers, without any symbols');
            printHelp('','/regenerate','Regenerate a new password (with symbols, letters and numbers)');
            print "Both /alphanumeric and /regenerate can take a single parameter,\n";
            print "the length of the password to be generated. Ie. /alphanumeric 15\n";
            print "will generate a 15-character long alphanumeric password\n";
        }
        # Generate a alphanumeric-only password
        elsif(index($password,'/alphanumeric') == 0)
        {
            $alphaNumeric = 1;
        }
        # Generate a new password
        elsif(index($password,'/regenerate') == 0)
        {
            $alphaNumeric = 0;
        }
        else
        {
            last;
        }
        # Handle a length-parameter supplied to /regenerate or /alphanumeric
        if ($password =~ s{^/(regenerate|alphanumeric)\s+(\d+)\s*}{$2})
        {
            if ($password < 0 && $password > 1000)
            {
                print 'The password length must be higher than zero'."\n";
            }
            else
            {
                $length = $password;
            }
        }
        else
        {
            $length = $defaultLength;
        }
    }

    if(defined $existing)
    {
        print 'Changed '.$name.' from '.$existing.' to '.$password."\n";
    }
    $data->{pwds}->{$name} = gpgEncryptString($password,true);
}

# Purpose: Get a fuzzy regexp for simple typos or missing characters
sub getTypoOrMissingRegex
{
    my $pattern = shift;
    my $fuzzyness = shift;

    # This is done by first removing any 'non-word' character.
    # Then each part is split into a regex that accepts the
    # current, previous and next character in the word, as well as $fuzzyness
    # characters after this one, which can be anything.
    # 
    # Ie. the $name 'test', and $fuzzy=1 will become:
    # [te][tes][est][st]
    # 
    # Whereas $fuzzy=4 will become:
    # [te].?.?.?[tes].?.?.?[est].?.?.?[st].?.?.?
    my @parts = split('',$pattern);
    $pattern = '';
    my $prev = '';
    my $fuzzyNo = $fuzzyness;
    my $fuzzyString = '';
    while($fuzzyNo--)
    {
        $fuzzyString .= '.?';
    }
    for(my $i = 0; $i < @parts; $i++)
    {
        my $part ='';
        if ($i != 0)
        {
            $part .= $parts[$i-1];
        }
        $part .= $parts[$i];
        if ( defined $parts[$i+1])
        {
            $part .= $parts[$i+1];
        }
        $pattern .= '['.$part.']'.$fuzzyString;
    }
    return (qr/$pattern/i,$pattern);
}

# Purpose: Get passwords from the database and output them to the user
# Usage: get($data,NAME);
# $data is the data hashref
# NAME is the regex to search for
sub get
{
    my $data        = shift;
    my $name        = shift;
    my $fuzzySearch = 0;
    my $matches     = {};

    foreach my $fuzzy (0..10)
    {
        my $grantsStartEndBonus = true;
        my $pattern = $name;

        if ($fuzzy > 1)
        {
            $pattern =~ s/\W//g;
        }

        my $regex;
        if ($fuzzy == 0)
        {
            $regex = qr/$name/i or pDie('Failed to parse "'.$name.'" as a perl regular expression'."\n");
        }
        # Fuzzy method: Multiple words in wrong order
        # Fuzzy method: Multiple words, where one of them is wrong
        elsif($fuzzy == 1 || $fuzzy == 8)
        {
            my @parts = split(/\W/,$name);
            next if scalar(@parts) == 1;
            my $expr = '('.join('|',@parts).')';
            $pattern = '';
            if ($fuzzy == 1)
            {
                while(defined shift(@parts))
                {
                    if ($pattern)
                    {
                        $pattern .= '.+';
                    }
                    $pattern .= $expr;
                }
            }
            elsif($fuzzy == 8)
            {
                $pattern = $expr;
            }
            $regex = qr/$pattern/i;
        }
        # Fuzzy method 2: Simple typos or missing characters
        elsif ($fuzzy == 2)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,true);
        }
        # Extra characters, or other mistakes, inside the word, but approx.
        # the correct length
        elsif($fuzzy == 3)
        {
            # This is based upon the assumption that the first and last
            # characters are correct, and that all characters that we need are
            # present. Additionally it assumes that the length is approximately
            # correct
            #
            # All non-word characters are removed.
            my @parts = split('',$pattern);
            my $minLength = scalar(@parts)-3;
            my $maxLength = scalar(@parts)+1;
            $pattern = $parts[0].'['.$pattern.']{'.$minLength.','.$maxLength.'}'.$parts[-1];
            $regex = qr/$pattern/;
        }
        # Simple typos or missing characters
        elsif ($fuzzy == 4)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,2);
        }
        # Simple typos or missing characters
        elsif ($fuzzy == 5)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,3);
        }
        # Simple typos or missing characters
        elsif ($fuzzy == 6)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,4);
        }
        # Extra characters, or other mistakes, inside the word
        elsif ($fuzzy == 7 || $fuzzy == 11)
        {
            # This is based upon the assumption that the first and last characters
            # are correct, and that all characters that we need are present.
            #
            # All non-word characters are removed.
            my @parts = split('',$pattern);
            $pattern = $parts[0].'['.$pattern.']+'.$parts[-1];
            if ($fuzzy == 6)
            {
                $pattern = '^'.$pattern.'$';
                $grantsStartEndBonus = false;
            }
            $regex = qr/$pattern/;
        }
        # Correct length, incorrect order
        elsif ($fuzzy == 9)
        {
            # This is very general, all non-word characters are removed, and
            # we construct a character class consisting of all of the remaining
            # characters. This character class has to be present at least length($word)
            # times. This only works for short words, and isn't used if length > 8
            next if length($name) > 8;
            $pattern = '['.$pattern.']{'.length($pattern).'}';
            $regex = qr/$pattern/;
        }
        # Fuzzy method: Acronym detection
        elsif($fuzzy == 10)
        {
            # This is about as general as it can get. We assume all characters are
            # present, but that it was added as an acronym. Ie. "example site" could
            # be "es". It's extremely general and thus any match gets labelled
            # "very fuzzy", instead of just "fuzzy". It also isn't used if the expression
            # is longer than 12 characters.
            next if length($name) > 12;
            $fuzzySearch++;

            $pattern = '^['.$pattern.']+$';
            $regex = qr/$pattern/i;
            $grantsStartEndBonus = false;
        }
        else
        {
            pDie('Attempted to use unknown fuzzy method no. '.$fuzzy);
        }

        if ($fuzzy)
        {
            printv(V_LOG,'Trying fuzzy regex ('.$fuzzy.'): '.$pattern."\n");
        }

        getMatches($matches,$data,$regex,$name,$fuzzy,$grantsStartEndBonus);

        if ($allMatches)
        {
            next;
        }

        if(keys %{$matches})
        {
            if ($fuzzy)
            {
                $fuzzySearch++;
            }
            last;
        }
    }
    statusOut('');

    if (! (keys %{$matches}))
    {
        print '(no passwords found for "'.$name.'")'."\n";
        return;
    }

    print 'Passwords:';
    if ($allMatches)
    {
        print ' (showing all matches, including very fuzzy ones)';
    }
    elsif ($fuzzySearch)
    {
        print ' (found using ';
        if ($fuzzySearch > 1)
        {
            print 'very ';
        }
        print 'fuzzy search)';
    }
    print "\n";

    my $entries = scalar(keys(%{ $matches }));
    my $entryNo = 0;
    foreach my $entry (sort { $matches->{$a}->{score} <=> $matches->{$b}->{score} } keys %{$matches})
    {
        $entryNo++;
        outputEntry($entry,$matches->{$entry}, $entryNo == 1);
    }
}

# Purpose: Get data entries that match a given regex
# Usage: $matches = getMatches($data,$regex,$name);
sub getMatches
{
    my $matches             = shift;
    my $data                = shift;
    my $regex               = shift;
    my $name                = shift;
    my $score               = shift;
    my $grantsStartEndBonus = shift;
    my $searchLength        = length($name);
    my $fuzzyNo             = $score;

    foreach my $key (sort keys %{$data->{pwds}})
    {
        next if defined $matches->{$key};
        if ($key =~ $regex)
        {
            my $entryScore = $score;
            if ($key eq $name)
            {
                $entryScore -= 5;
            }
            else
            {
                # Add a 0.5 penalty for each character above the search string length
                # IF the $name is only alphanumeric
                if ($name !~ /\W/)
                {
                    my $keyLen = length($key);
                    if ($keyLen > $searchLength)
                    {
                        my $penalty = ($keyLen-$searchLength)*0.25;
                        printv(V_DEBUG,'Penalizing '.$key.' '.$penalty.' due to its length'."\n");
                        $entryScore += ($keyLen-$searchLength)*0.25;
                    }
                }
                # Grant a bonuses if the regex matched at the beginning or end, if
                # requested
                if ($grantsStartEndBonus)
                {
                    my ($bonus,$reason);
                    # Grant a -3 bonus for matching the regex at the start of
                    # the string followed by a non-word character and the fuzzy
                    # method in use is 0
                    if ($fuzzyNo == 0 && $key =~ /^$regex\S/)
                    {
                        $bonus = -3;
                        $reason = 'start-nonword';
                    }
                    # Grant a -2 bonus for matching the regex at the start of the string
                    elsif ($key =~ /^$regex/)
                    {
                        $bonus = -2;
                        $reason = 'start';
                    }
                    # Grant a -1 bonus for matching the regex at the end of the string
                    elsif($key =~ /$regex$/)
                    {
                        $bonus = -1;
                        $reason = 'end';
                    }
                    if ($reason)
                    {
                        printv(V_DEBUG,'Granting a '.$bonus.' bonus to '.$key.' due to '.$reason.' match'."\n");
                        $entryScore += $bonus;
                    }
                }
            }
            $matches->{$key} = {
                password => $data->{pwds}->{$key},
                score => $entryScore,
            };
        }
    }
    return $matches;
}

# Purpose: Output a password
# Usage: outputEntry(KEY,VALUE,COPY);
# KEY is the title
# VALUE is the content (ie. password)
# COPY is a bool, if true it will toClipboard() the VALUE
sub outputEntry
{
    my $key     = shift;
    my $content = shift;
    my $copy    = shift;
    my $copied  = '';
    printf('%-20s: %s',$key,'...');
    my $value = gpgDecryptString($content->{password},true);
    if ($copy)
    {
        $copied = toClipboard($value);
    }

    printv(V_LOG,$key.' had a score of '.$content->{score}."\n");
    printf("\r".'%-20s: %s'."\n",$key,$value.$copied);
}

# Purpose: Perform git actions
# Usage: git(ACTION,PATH);
# PATH is the path to the data file we are operating on
# ACTION is one of:
#   pull       Pull changes
#   safepull   Pull changes IF we have an ssh agent
#   push       Push changes
sub git
{
    my $command = shift;
    my $path    = shift;

    if (!$enableGit)
    {
        return;
    }

    my $cwd = getcwd;
    chdir(dirname(realpath($path)));

    if ($command eq 'safepull')
    {
        if (
            (defined $ENV{SSH_AGENT_PID}) ||
            (defined $ENV{GNOME_KEYRING_PID} && defined $ENV{SSH_AUTH_SOCK})
        )
        {
            $command = 'pull';
        }
        else
        {
            $command = 'noop';
            printv(V_INFO,'Not pulling since SSH_AGENT_PID is not set, and neither is GNOME_KEYRING_PID and SSH_AUTH_SOCK'."\n");
        }
    }
    if($command eq 'pull')
    {
        print "\n(git pulling): ";
        if (psystem('git','pull','--rebase','--quiet') != 0)
        {
            if(psystem('git','pull','--quiet') != 0)
            {
                pDie('Failed to git pull, you must manually resolve the conflict'."\n");
            }
        }
        elsif ($verbosity == 0)
        {
            print "\r";
        }
    }
    elsif($command eq 'push')
    {
        print 'Pushing git repository...'."\n";
        psystem('git','add',basename($path));
        psystem('git','commit','--quiet','-m','Update by gpgpwd',basename($path));
        psystem('git','push','--quiet');
    }
    elsif($command ne 'noop')
    {
        printv(V_INFO,'WARNING: Unknown command "'.$command.'" in git()'."\n");
    }

    chdir($cwd);
}

# Purpose: Output our usage information and (optionally) exit
# Usage: usage(N);
#  If N is supplied, will pExit(N) after outputting.
sub usage
{
    my $exitValue = shift;

    print "\n";
    print 'USAGE: '.basename($0).' [OPTIONS]? [COMMAND] [PARAMETERS]'."\n";
    print "\n";
    print "Options:\n";
    printHelp(''   , '--help'            , 'View this help screen');
    printHelp(''   , '--version'         , 'Display version information and exit');
    printHelp('-v' , '--verbose'         , 'Increase verbosity (can be supplied multiple times)');
    printHelp('-p' , '--password-file'   , 'Save passwords to this file instead of ~/.gpgpwddb');
    printHelp('-g' , '--git'             , 'Use git to synchronize the password file (see the manpage for details)');
    printHelp('-G' , '--no-git'          , 'Disable git mode (overriding --git)');
    printHelp('-C' , '--no-xclip'        , 'Disable copying of passwords to the clipboard when running under X');
    printHelp('-c' , '--xclip-clipboard' , 'Use the clipboard supplied instead of the default (see the manpage for details)');
    printHelp(''   , '--all'             , 'Return all possible results for "get", even very fuzzy results (default: return only the best results)');
    printHelp(''   , '--debuginfo'       , 'Display some information that can be useful for debugging');
    print "\n";
    print "Commands:\n";
    printHelp('' , 'get [name]'         , 'Get password for [name] (where [name] can be a perl-compatible regular expression)');
    printHelp('' , 'set [name]'         , 'Add or change password for [name]');
    printHelp('' , 'remove [name]'      , 'Remove the entry for [name]');
    printHelp('' , 'rename [old] [new]' , 'Rename the entry for [old] to [new]');
    printHelp('' , 'batchadd [file]'    , 'Batch add passwords from a file, see the manpage for the file syntax');
    printHelp('' , 'upgrade'            , 'Upgrade an old database file to the new format if needed');

    if (defined $exitValue)
    {
        pExit($exitValue);
    }
}

# Purpose: Print formatted --help output
# Usage: printHelp('-shortoption', '--longoption', 'description');
#  Description will be reformatted to fit within a normal terminal
sub printHelp
{
    # The short option
    my $short = shift,
    # The long option
    my $long = shift;
    # The description
    my $desc = shift;
    # The generated description that will be printed in the end
    my $GeneratedDesc;
    # The current line of the description
    my $currdesc = '';
    # The maximum length any line can be
    my $maxlen = 80;
    # The length the options take up
    my $optionlen = 20;
    # Check if the short/long are LONGER than optionlen, if so, we need
    # to do some additional magic to take up only $maxlen.
    # The +1 here is because we always add a space between them, no matter what
    if ((length($short) + length($long) + 1) > $optionlen)
    {
        $optionlen = length($short) + length($long) + 1;
    }
    # Split the description into lines
    foreach my $part (split(' ',$desc))
    {
        if(defined $GeneratedDesc)
        {
            if ((length($currdesc) + length($part) + 1 + 24) > $maxlen)
            {
                $GeneratedDesc .= "\n";
                $currdesc = '';
            }
            else
            {
                $currdesc .= ' ';
                $GeneratedDesc .= ' ';
            }
        }
        $currdesc .= $part;
        $GeneratedDesc .= $part;
    }
    # Something went wrong
    pDie('Option mismatch') if not $GeneratedDesc;
    # Print it all
    foreach my $description (split("\n",$GeneratedDesc))
    {
        printf "%-4s %-19s %s\n", $short,$long,$description;
        # Set short and long to '' to ensure we don't print the options twice
        $short = '';$long = '';
    }
    # Succeed
    return true;
}

# Purpose: Get the version of a shell utility
# Usage: version = getVersionFrom('command');
sub getVersionFrom
{
    if (!InPath($_[0]))
    {
        return 'not installed';
    }
    open3(my $in, my $out, my $err,@_);
    my $data;
    if ($out)
    {
        while(<$out>)
        {
            $data .= $_;
        }
    }
    if ($err)
    {
        while(<$err>)
        {
            $data .= $_;
        }
        close($err);
    }
    close($in);close($out);
    $data =~ s/^\D+(\S+).+/$1/s;
    return $data;
}

# Purpose: Output some information useful for debugging and then exit
# Usage: debugInfo();
sub debugInfo
{
    autoInitGPGAgent();

    print "gpgpwd version $VERSION\n";
    print "\n";
    my $pattern = "%-28s: %s\n";
    printf($pattern , 'Data file'         , $storagePath);
    printf($pattern , 'Data version'      , $dataVersion);
    printf($pattern , 'Perl version'      , sprintf('%vd', $^V));
    my $gpgV = getVersionFrom('gpg'       , '--version');
    if (InPath('gpg2') && -l InPath('gpg') && basename(readlink(InPath('gpg'))) eq 'gpg2')
    {
        $gpgV = '(symlinked to gpg2)';
    }
    printf($pattern , 'gpg version'       , $gpgV);
    printf($pattern , 'gpg2 version'      , getVersionFrom('gpg2'      , '--version'));
    printf($pattern , 'gpg-agent version' , getVersionFrom('gpg-agent' , '--version'));
    printf($pattern , 'xclip version'     , getVersionFrom('xclip'     , '-version'));
    printf($pattern , 'git version'       , getVersionFrom('git'       , '--version'));

    my $flags = '';
    foreach my $flag (qw(enableGit useXclip allMatches))
    {
        if (! eval('$'.$flag))
        {
            next;
        }
        $flags .= $flag.' ';
    }
    if ($gpg[0] eq 'gpg2')
    {
        $flags .= 'useGPGv2';
    }
    else
    {
        $flags .= 'useGPGv1';
    }
    if ($autoGPGAgent > -1)
    {
        $flags .= ' autoGPGAgent';
    }
    printf($pattern,'flags',$flags);

    eval('use Digest::MD5;');
    my $md5 = Digest::MD5->new();
    my $self = $0;
    if(not -f $self)
    {
        $self = InPath($self);
    }
    open(my $f,'<',$self);
    $md5->addfile($f);
    my $digest = $md5->hexdigest;
    close($f);
    printf($pattern,'MD5',$digest);

    pExit(0);
}

# Purpose: Main entry point
# Usage: main()
sub main
{
    $| = true;

    my $noGit     = false;
    my $debugInfo = false;

    if (@ARGV == 0)
    {
        usage(0);
    }

    Getopt::Long::Configure('no_ignore_case','bundling');

    GetOptions(
        'help' => sub {
            usage(0);
        },
        'version' => sub
        {
            print 'gpgpwd version '.$VERSION."\n";
            pExit(0);
        },
        'v|verbose+' => \$verbosity,
        'p|password-file=s' => \$storagePath,
        'g|git|i|fast-git' => \$enableGit,
        'G|no-git' => \$noGit,
        'debuginfo' => \$debugInfo,
        'C|no-xclip' => sub { $useXclip = false; },
        'c|xclip-clipboard=s' => sub
        {
            shift;
            my $value = shift;

            if ($value eq 'both')
            {
                @clipboardTargets = qw(primary clipboard);
            }
            elsif($value eq 'clipboard')
            {
                @clipboardTargets = qw(clipboard);
            }
            elsif($value eq 'selection')
            {
                @clipboardTargets = qw(primary);
            }
            else
            {
                pDie('Unknown value for --xclip-cilpboard: '.$value."\n");
            }
        },
        'r|require-agent' => sub
        {
            print 'Note: --require-agent is a no-op as of version 0.4'."\n";
        },
        'R|no-require-agent' => sub
        {
            print 'Note: --no-require-agent is a no-op as of version 0.4'."\n";
        },
        'T|t|try-require-agent' => sub
        {
            print 'Note: --try-require-agent is the default as of version 0.4'."\n";
        },
        'disable-agent' => sub {
            print 'Note: --disable-agent is a no-op as of version 0.4. The agent is required.'."\n";
        },
        'all' => \$allMatches,
    ) or pDie('See --help for more information'."\n");

    my $command = shift(@ARGV);

    if (!$command && !$debugInfo)
    {
        usage(0);
    }

    if (!InPath('gpg') && !InPath('gpg2'))
    {
        pDie('Failed to locate gpg which is required for gpgpwd to work, unable to continue'."\n");
    }
    elsif (
            # If we don't have gpg(1) then we need to use gpg2
            !InPath('gpg')
            # If we have both gpg(1) and gpg2, check if gpg is a symlink to gpg2,
            # if it is then enable gpg2-mode
            || (InPath('gpg2') && -l InPath('gpg') && basename(readlink(InPath('gpg'))) eq 'gpg2')
        )
    {
        # Note: If gpg isn't installed then we *know* gpg2 is installed,
        # because if neither is installed we refuse to run at all (test at the
        # top of main() )
        printv(V_INFO,'Using gpg2 instead of gpg'."\n");
        $gpg[0] = 'gpg2';
    }

    # Verify that we're able to read and write to $storagePath, as well as
    # the directory $storagePath is located in (for backup files).
    if (-e $storagePath && ! -r $storagePath)
    {
        pDie($storagePath.': is not readable');
    }
    elsif (-e $storagePath && ! -w $storagePath)
    {
        pDie($storagePath.': is not writeable'."\n");
    }
    if ( ! -w dirname($storagePath) )
    {
        if (-e $storagePath)
        {
            warn('WARNING: '.dirname($storagePath).' is not writable, gpgpwd may be unable'."\n".'to create backup files!'."\n");
        }
        else
        {
            pDie(dirname($storagePath).': is not writeable'."\n");
        }
    }

    if(gpgAgentRunning)
    {
        # Add --no-tty to gpg's parameter list to reduce its output and
        # require a gpg-agent.
        push(@gpg,'--no-tty');
        # Explicitly enable the agent for gpg1
        if ($gpg[0] ne 'gpg2')
        {
            push(@gpg,'--use-agent');
        }
    }

    # Disable --git if --no-git was supplied
    if ($noGit)
    {
        $enableGit   = false;
    }

    if ($debugInfo)
    {
        debugInfo();
    }
    elsif ($command eq 'get')
    {
        if(! @ARGV)
        {
            warn('Missing parameter to get: what to retrieve'."\n");
            usage(103);
        }
        elsif(@ARGV != 1)
        {
            pDie('Too many parameters for "get"'."\n");
        }

        my $data = loadData($storagePath);
        get($data,@ARGV);

        if ($enableGit)
        {
            my $fileID = getFileID($storagePath);
            git('safepull',$storagePath);
            # If the file has changed due to the pull, then we re-fetch
            if ($fileID ne getFileID($storagePath))
            {
                print "\n";
                print "File updated by git, re-reading passwords:\n";
                $data = loadData($storagePath);
                get($data,@ARGV);
            }
        }
    }
    elsif($command eq 'batchadd')
    {
        if (!@ARGV)
        {
            warn('Missing parameter to batchadd: path to the file to read'."\n");
            usage(104);
        }
        elsif(@ARGV != 1)
        {
            pDie('Too many parameters for "batchadd"'."\n");
        }
        git('pull',$storagePath);
        my $data = loadData($storagePath);
        statusOut('');
        loadFromFile(shift(@ARGV),$data);
        writeData($storagePath,$data);
    }
    elsif($command eq 'remove')
    {
        if (!@ARGV)
        {
            warn('Missing parameter to remove: what to remove'."\n");
            usage(104);
        }
        elsif(@ARGV != 1)
        {
            pDie('Too many parameters for "remove"'."\n");
        }
        my $name = shift(@ARGV);

        git('pull',$storagePath);
        my $data = loadData($storagePath);
        statusOut('');
        if ($data->{pwds}->{$name})
        {
            print 'Removed '.$name.' (with the password '.gpgDecryptString($data->{pwds}->{$name},true).')'."\n";
            delete($data->{pwds}->{$name});
        }
        else
        {
            print 'No entry named '.$name.' found. Doing nothing.'."\n";
            pExit(0);
        }
        writeData($storagePath,$data);
    }
    elsif($command eq 'set' || $command eq 'add' || $command eq 'change')
    {
        if (!@ARGV)
        {
            warn('Missing parameter to '.$command.': what to set'."\n");
            usage(104);
        }
        elsif(@ARGV != 1)
        {
            pDie('Too many parameters for "'.$command.'" (note that you will be prompted for a password)'."\n");
        }
        git('pull',$storagePath);
        my $data = loadData($storagePath);
        set($data,@ARGV);
        writeData($storagePath,$data);
    }
    elsif($command eq 'rename')
    {
        my $old = shift(@ARGV);
        my $new = shift(@ARGV);
        if (!defined $old)
        {
            warn('Missing parameters to rename: old name, new name'."\n");
            usage(104);
        }
        elsif (!defined $new)
        {
            warn('Missing parameters to rename: new name'."\n");
            usage(104);
        }
        elsif(@ARGV != 0)
        {
            pDie('Too many parameters for "rename"'."\n");
        }
        git('pull',$storagePath);
        my $data = loadData($storagePath);
        statusOut('');

        my $entry = $data->{pwds}->{$old};
        if (!defined $entry)
        {
            pDie('Failed to find "'.$old.'". Note that you must specify the exact name when'."\n".'using "rename", as it does no fuzzy searching'."\n");
        }
        $data->{pwds}->{$new} = $entry;
        delete($data->{pwds}->{$old});

        print 'Renamed the entry for '.$old.' to '.$new."\n";

        writeData($storagePath,$data);
    }
    elsif($command eq 'upgrade')
    {
        upgradeDataV1toV2();
    }
    else
    {
        if (@ARGV == 0)
        {
            print "$command is an unknown command. Maybe you meant:\n";
            print basename($0).' get '.$command."\n";
            print '  or'."\n";
            print basename($0).' set '.$command."\n";
            print "\nSee ".basename($0)." --help for more information.\n";
            exit(103);
        }
        else
        {
            warn "Unknown command: $command\n";
            usage(102);
        }
    }
    pExit(0);
}

main();
__END__

=encoding utf8

=head1 NAME

gpgpwd - a command-line password manager based around GnuPG

=head1 SYNOPSIS

B<gpgpwd> [I<OPTIONS>] [I<COMMAND>] [I<PARAMETERS>]

=head1 DESCRIPTION

B<gpgpwd> is a terminal-based password manager. It stores a list of passwords
in a GnuPG encrypted file, and allows you to easily retrieve, change and add to
that file as needed. It also generates random passwords that you can use,
easily allowing you to have one "master password" (for your gpg key), with
one unique and random password for each website or service you use, ensuring
that your other accounts stay safe even if one password gets leaked.

B<gpgpwd> can also utilize git(1) to allow you to easily synchronize your
passwords between different machines.

=head1 OPTIONS

=over

=item B<--help>

Display the help screen

=item B<--version>

Output the gpgpwd version and exit

=item B<-v, --verbose>

Increase gpgpwd verbosity. May be supplied multiple times to further increase
verbosity.

=item B<-p, --password-file> I<FILE>

Set the password file to I<FILE> instead of the default. This changes where
gpgpwd reads and writes the password database.

You may supply several --password-file arguments, but only the last one
will be used.

=item B<-g, --git>

Enables git(1) mode. This can be used to keep a password database in sync
between several different computers.  This causes gpgpwd to I<git pull> before
it writes any change to the file, and I<git commit> and I<git push> after a
change has been made. In read mode it will I<git pull> after getting a
password, if it detects that the password file has changed after pulling,
gpgpwd will process your get request again, in case the password you wanted has
changed.

It requires you to configure a git repository that contains your password file,
and a central server that gpgpwd can push to and pull from. You will need to
initialize and set up the git repository yourself. You will either need to use
I<--password-file> to provide the path to the file in your git repository, or
symlink I<~/.gpgpwddb> to the password file located in your git repository.

=item B<-G, --no-git>

Disables git(1) mode. This will override any --git parameters that you have
specified. This can be useful if you have a shell alias that adds --git to
the default gpgpwd parameters, but you want to operate on a different
--password-file that is not in git.

=item B<-C, --no-xclip>

Disables use of xclip(1). By default gpgpwd will copy passwords to the clipboard
for easy pasting into password fields. When this option is supplied it supresses
this behaviour.

=item B<-c, --xclip-clipboard> I<CLIPBOARD>

By default gpgpwd will copy passwords to the clipboard (the one that pastes through
the usual I<ctrl+v> or "right click -> paste" means). With this you can change it.
It accepts the following parameters as I<CLIPBOARD>:

=over

=item I<clipboard>

The default, copies to the 'normal' clipboard. Paste with ie. I<ctrl+v>.

=item I<selection>

Copy to the 'selection' clipboard. Paste with ie. middle-click.

=item I<both>

Copy to both the 'normal' and 'selection' clipboards.

=back

=item B<--all>

Return all posible results for a "get" request. This includes very fuzzy
results.  The default, which is to return only the best results, is usually
preferable to I<--all>.

=item B<--debuginfo>

Display some information useful for debugging.

=back

=head1 COMMANDS

=over

=item B<get> I<NAME>

Get the password for NAME. NAME can be a perl-compatible regular expression. If
no matches are found gpgpwd will attempt to perform a fuzzy search, to see if
something similar can be found (ie. to correct for typos).

If you want to retrieve your entire database you may simply supply . as NAME,
since it accepts a regular expression and . will match everything.

=item B<set> I<NAME>

Set (add or change) the password for NAME. You will be prompted interactively for the
password, and will be given a random password that you may use if you wish.

=item B<remove> I<NAME>

Remove the password for NAME from the database.

=item B<rename> I<OLDNAME> I<NEWNAME>

Rename the entry for OLDNAME to NEWNAME.

=item B<batchadd> I<FILE>

Read and add a list of passwords from FILE. The format is simple:

    NAME PASSWORD

Everything up until the first bit of whitespace is taken to be the name,
and everything from the first non-whitespace character after that and
until the end of the line is taken to be the password. It will ignore
empty lines and lines starting with #.

=item B<upgrade>

Upgrades a database file using the old format (v1, used by gpgpwd 0.3 and
older) to the new format (v2, used by gpgpwd 0.4 and later).

=back

=head1 EXAMPLES

=over

=item gpgpwd set test

Add a password for 'test' to the database, gpgpwd will prompt you for the password.

=item gpgpwd get test

Retrieve the password we just added.

=item gpgpwd remove test

Remove test from the adatabase

=item gpgpwd -g set testpwd

Add the password for testpwd to the database and commit+push the file using
git afterwards.

=item gpgpwd --xclip-clipboard both get testpwd

Get the password for testpwd, copying it to both the selection and regular
clipboards.

=item gpgpwd rename testpwd test-password

Rename 'testpwd' to 'test-password'.

=back

=head1 HELP/SUPPORT

If you need additional help, please visit the website at
L<http://random.zerodogg.org/gpgpwd>

=head1 BUGS AND LIMITATIONS

If you find a bug, please report it at L<http://random.zerodogg.org/gpgpwd/bugs>

Include the output of 'gpgpwd --debuginfo' in any bug report.

=head1 AUTHOR

B<gpgpwd> is written by Eskild Hustvedt I<<code aatt zerodogg d0t org>>

=head1 FILES

=over

=item I<~/.gpgpwddb>

The default save location for the password database, overrideable by using
I<--password-file>.

=back

=head1 LICENSE AND COPYRIGHT

Copyright (C) Eskild Hustvedt 2012, 2013, 2014

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see L<http://www.gnu.org/licenses/>.
