The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Passwd::Keyring::OSXKeychain;

use warnings;
use strict;

use Carp qw(croak);
use IPC::System::Simple qw(capturex systemx);

=head1 NAME

Passwd::Keyring::OSXKeychain - Password storage implementation based on OSX/Keychain.

=head1 VERSION

Version 0.1006

=cut

our $VERSION = '0.1006';

=head1 WARNING

I do not have Mac. I wrote the library mimicking actions
of some python libraries and tested using mocks, but help
of somebody able to test it on true Mac is really needed.

=head1 SYNOPSIS

OSXKeychain Keyring based implementation of L<Keyring>. Provide secure
storage for passwords and similar sensitive data.

    use Passwd::Keyring::OSXKeychain;

    my $keyring = Passwd::Keyring::OSXKeychain->new(
         app=>"blahblah scraper",
         group=>"Johnny web scrapers",
    );

    my $username = "John";  # or get from .ini, or from .argv...

    my $password = $keyring->get_password($username, "blahblah.com");
    unless( $password ) {
        $password = <somehow interactively prompt for password>;

        # securely save password for future use
        $keyring->set_password($username, $password, "blahblah.com");
    }

    login_somewhere_using($username, $password);
    if( password_was_wrong ) {
        $keyring->clear_password($username, "blahblah.com");
    }

Note: see L<Passwd::Keyring::Auto::KeyringAPI> for detailed comments
on keyring method semantics (this document is installed with
C<Passwd::Keyring::Auto> package).

=head1 SUBROUTINES/METHODS

=head2 new(app=>'app name', group=>'passwords folder')

Initializes the processing. Croaks if osxkeychain keyring does not 
seem to be available.

Handled named parameters: 

- app - symbolic application name (not used at the moment, but can be
  used in future as comment and in prompts, so set sensibly)

- group - name for the password group (will be visible in seahorse so
  can be used by end user to manage passwords, different group means
  different password set, a few apps may share the same group if they
  need to use the same passwords set)

(OSXKeychain-specific)

- security_prog - location of security program (/usr/bin/security by
  default, possibility to overwrite is mostly needed for testing)

- keychain - keychain to use (if not default)

=cut

sub new {
    my ($cls, %opts) = @_;
    my $self = {
        app => $opts{app} || 'Passwd::Keyring',
        group => $opts{group} || 'Passwd::Keyring unclassified passwords',
        security => $opts{security_prog} || '/usr/bin/security',
        keychain => $opts{keychain},
    };
    bless $self, $cls;

    unless( -x $self->{security} ) {
        croak("OSXKeychain not available: security program $self->{security} is missing");
    }
    if($self->{keychain}) {
        # Add .keychain suffix if missing
        $self->{keychain} .= '.keychain' 
          unless $self->{keychain} =~ /\.keychain$/;
    } 

    # Some test operation
    my $reply = capturex($self->_make_sys_args(
        "-q",
        "show-keychain-info"));

    return $self;
}

# Prepares args by prefixing with command and suffixing with keychain
# if specified
sub _make_sys_args {
    my ($self, @args) = @_;
    unshift @args, $self->{security};
    push @args, $self->{keychain} if $self->{keychain};
    return @args;
}

=head2 set_password(username, password, realm)

Sets (stores) password identified by given realm for given user 

=cut

sub set_password {
    my ($self, $user_name, $user_password, $realm) = @_;

    # TODO: maybe use -l (label) instead of -D
    systemx($self->_make_sys_args(
        "-q", # quiet
        "add-generic-password",
        "-a", $user_name,
        "-s", $realm,
        "-D", $self->{group}, # "kind", can be used to match so let be
        "-w", $user_password,
        "-j", $self->{app}, # comment
        "-A", # any app can access
        "-U", # allow update
        ));
}

sub _parse_password_from_find_output {
    my ($text) = @_;

    if($text =~ /^ *password: *"([^"]*)"/m)  {
        return $1;
    }
    elsif($text =~ /^ *password: *\$([0-9A-Fa-f]*)/m) {
        return pack("H*", $1);
    }
    elsif($text =~ /^ *password: *$/m)  {
        return "";
    }

}

=head2 get_password($user_name, $realm)

Reads previously stored password for given user in given app.
If such password can not be found, returns undef.

=cut

sub get_password {
    my ($self, $user_name, $realm) = @_;

    my $reply = capturex(
        [0, 44],   # Legal exit values. Some CpanTesters report 44 on password not found
        $self->_make_sys_args(
            "-q", # quiet
            "find-generic-password",
            "-a", $user_name,
            "-s", $realm,
            "-D", $self->{group}, # "kind", can be used to match so let be
            "-g", # display the password
        ));

    return _parse_password_from_find_output($reply);
}

=head2 clear_password($user_name, $realm)

Removes given password (if present)

Returns how many passwords actually were removed 

=cut

sub clear_password {
    my ($self, $user_name, $realm) = @_;

    my $reply = systemx($self->_make_sys_args(
        "delete-generic-password",
        "-a", $user_name,
        "-s", $realm,
        "-D", $self->{group}, # "kind", can be used to match so let be
        ));

}

=head2 is_persistent

Returns info, whether this keyring actually saves passwords persistently.

(true in this case)

=cut

sub is_persistent {
    my ($self) = @_;
    return 1;
}


=head1 AUTHOR

Marcin Kasperski

=head1 BUGS

Please report any bugs or feature requests to 
issue tracker at L<https://bitbucket.org/Mekk/perl-keyring-osxkeychain>.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Passwd::Keyring::OSXKeychain

You can also look for information at:

L<http://search.cpan.org/~mekk/Passwd-Keyring-OSXKeychain/>

Source code is tracked at:

L<https://bitbucket.org/Mekk/perl-keyring-osxkeychain>

=head1 LICENSE AND COPYRIGHT

Copyright 2012 Marcin Kasperski.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut


1; # End of Passwd::Keyring::OSXKeychain