File Coverage

blib/lib/Passwd/Keyring/OSXKeychain.pm
Criterion Covered Total %
statement 20 43 46.5
branch 5 14 35.7
condition 0 6 0.0
subroutine 6 13 46.1
pod 5 5 100.0
total 36 81 44.4


line stmt bran cond sub pod time code
1             package Passwd::Keyring::OSXKeychain;
2              
3 11     11   272283 use warnings;
  11         28  
  11         502  
4 11     11   65 use strict;
  11         21  
  11         693  
5              
6 11     11   66 use Carp qw(croak);
  11         20  
  11         1056  
7 11     11   8561 use IPC::System::Simple qw(capturex systemx runx);
  11         204947  
  11         1223  
8 11     11   7300 use Capture::Tiny qw(capture_merged);
  11         406216  
  11         8552  
9              
10             # TODO: considering we use Capture::Tiny, maybe drop IPC::System::Simple
11             # and move to Capture::Tiny altogether (note that this means
12             # checking exit status and raising exceptions). Or at least
13             # drop all capturex.
14              
15             =head1 NAME
16              
17             Passwd::Keyring::OSXKeychain - Password storage implementation based on OSX/Keychain.
18              
19             =head1 VERSION
20              
21             Version 0.20
22              
23             =cut
24              
25             our $VERSION = '0.20';
26              
27             =head1 WARNING
28              
29             I do not have Mac. I wrote the library mimicking actions
30             of some python libraries and tested using mocks, but help
31             of somebody able to test it on true Mac is really needed.
32              
33             =head1 SYNOPSIS
34              
35             OSXKeychain Keyring based implementation of L. Provide secure
36             storage for passwords and similar sensitive data.
37              
38             use Passwd::Keyring::OSXKeychain;
39              
40             my $keyring = Passwd::Keyring::OSXKeychain->new(
41             app=>"blahblah scraper",
42             group=>"Johnny web scrapers",
43             );
44              
45             my $username = "John"; # or get from .ini, or from .argv...
46              
47             my $password = $keyring->get_password($username, "blahblah.com");
48             unless( $password ) {
49             $password = ;
50              
51             # securely save password for future use
52             $keyring->set_password($username, $password, "blahblah.com");
53             }
54              
55             login_somewhere_using($username, $password);
56             if( password_was_wrong ) {
57             $keyring->clear_password($username, "blahblah.com");
58             }
59              
60             Note: see L for detailed comments
61             on keyring method semantics (this document is installed with
62             C package).
63              
64             =head1 SUBROUTINES/METHODS
65              
66             =head2 new(app=>'app name', group=>'passwords folder')
67              
68             Initializes the processing. Croaks if osxkeychain keyring does not
69             seem to be available.
70              
71             Handled named parameters:
72              
73             - app - symbolic application name (not used at the moment, but can be
74             used in future as comment and in prompts, so set sensibly)
75              
76             - group - name for the password group (will be visible in seahorse so
77             can be used by end user to manage passwords, different group means
78             different password set, a few apps may share the same group if they
79             need to use the same passwords set)
80              
81             (OSXKeychain-specific)
82              
83             - security_prog - location of security program (/usr/bin/security by
84             default, possibility to overwrite is mostly needed for testing)
85              
86             - keychain - keychain to use (if not default)
87              
88             =cut
89              
90             sub new {
91 0     0 1 0 my ($cls, %opts) = @_;
92 0   0     0 my $self = {
      0        
      0        
93             app => $opts{app} || 'Passwd::Keyring',
94             group => $opts{group} || 'Passwd::Keyring unclassified passwords',
95             security => $opts{security_prog} || '/usr/bin/security',
96             keychain => $opts{keychain},
97             };
98 0         0 bless $self, $cls;
99              
100 0 0       0 unless( -x $self->{security} ) {
101 0         0 croak("OSXKeychain not available: security program $self->{security} is missing");
102             }
103 0 0       0 if($self->{keychain}) {
104             # Add .keychain suffix if missing
105 0 0       0 $self->{keychain} .= '.keychain'
106             unless $self->{keychain} =~ /\.keychain$/;
107             }
108              
109             # Some test operation
110 0         0 my $reply = capturex($self->_make_sys_args(
111             "-q",
112             "show-keychain-info"));
113              
114 0         0 return $self;
115             }
116              
117             # Prepares args by prefixing with command and suffixing with keychain
118             # if specified
119             sub _make_sys_args {
120 0     0   0 my ($self, @args) = @_;
121 0         0 unshift @args, $self->{security};
122 0 0       0 push @args, $self->{keychain} if $self->{keychain};
123 0         0 return @args;
124             }
125              
126             =head2 set_password(username, password, realm)
127              
128             Sets (stores) password identified by given realm for given user
129              
130             =cut
131              
132             sub set_password {
133 0     0 1 0 my ($self, $user_name, $user_password, $realm) = @_;
134              
135             # TODO: maybe use -l (label) instead of -D
136 0         0 systemx($self->_make_sys_args(
137             "-q", # quiet
138             "add-generic-password",
139             "-a", $user_name,
140             "-s", $realm,
141             "-D", $self->{group}, # "kind", can be used to match so let be
142             "-w", $user_password,
143             "-j", $self->{app}, # comment
144             "-A", # any app can access
145             "-U", # allow update
146             ));
147             }
148              
149             sub _parse_password_from_find_output {
150 5     5   13 my ($text) = @_;
151              
152 5 100       87 if($text =~ /^ *password: *"([^"]*)"/m) {
    100          
    50          
153 2         12 return $1;
154             }
155             elsif($text =~ /^ *password: *\$([0-9A-Fa-f]*)/m) {
156 2         15 return pack("H*", $1);
157             }
158             elsif($text =~ /^ *password: *$/m) {
159 1         4 return "";
160             }
161              
162             }
163              
164             =head2 get_password($user_name, $realm)
165              
166             Reads previously stored password for given user in given app.
167             If such password can not be found, returns undef.
168              
169             =cut
170              
171             sub get_password {
172 0     0 1   my ($self, $user_name, $realm) = @_;
173              
174             my $reply = capture_merged {
175 0     0     runx(
176             [0, 44], # Legal exit values. Some CpanTesters report 44 on password not found
177             $self->_make_sys_args(
178             "-q", # quiet
179             "find-generic-password",
180             "-a", $user_name,
181             "-s", $realm,
182             "-D", $self->{group}, # "kind", can be used to match so let be
183             "-g", # display the password
184             ));
185 0           };
186 0           return _parse_password_from_find_output($reply);
187             }
188              
189             =head2 clear_password($user_name, $realm)
190              
191             Removes given password (if present)
192              
193             Returns how many passwords actually were removed
194              
195             =cut
196              
197             sub clear_password {
198 0     0 1   my ($self, $user_name, $realm) = @_;
199              
200 0           my $reply = systemx($self->_make_sys_args(
201             "delete-generic-password",
202             "-a", $user_name,
203             "-s", $realm,
204             "-D", $self->{group}, # "kind", can be used to match so let be
205             ));
206              
207             }
208              
209             =head2 is_persistent
210              
211             Returns info, whether this keyring actually saves passwords persistently.
212              
213             (true in this case)
214              
215             =cut
216              
217             sub is_persistent {
218 0     0 1   my ($self) = @_;
219 0           return 1;
220             }
221              
222              
223             =head1 AUTHOR
224              
225             Marcin Kasperski
226              
227             =head1 BUGS
228              
229             Please report any bugs or feature requests to
230             issue tracker at L.
231              
232             =head1 SUPPORT
233              
234             You can find documentation for this module with the perldoc command.
235              
236             perldoc Passwd::Keyring::OSXKeychain
237              
238             You can also look for information at:
239              
240             L
241              
242             Source code is tracked at:
243              
244             L
245              
246             =head1 LICENSE AND COPYRIGHT
247              
248             Copyright 2012 Marcin Kasperski.
249              
250             This program is free software; you can redistribute it and/or modify it
251             under the terms of either: the GNU General Public License as published
252             by the Free Software Foundation; or the Artistic License.
253              
254             See http://dev.perl.org/licenses/ for more information.
255              
256             =cut
257              
258              
259             1; # End of Passwd::Keyring::OSXKeychain
260